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..d22df7264 100644 --- a/commons/src/main/java/net/swofty/commons/config/ConfigProvider.java +++ b/commons/src/main/java/net/swofty/commons/config/ConfigProvider.java @@ -43,4 +43,5 @@ public class ConfigProvider { throw new RuntimeException("Failed to load configuration", e); } } + } 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..8dd0b5333 --- /dev/null +++ b/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/GetActivePunishmentProtocolObject.java @@ -0,0 +1,104 @@ +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 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 + 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()); + json.put("type", value.type()); + return json.toString(); + } + + @Override + public GetActivePunishmentMessage deserialize(String json) { + JSONObject obj = new JSONObject(json); + return new GetActivePunishmentMessage( + UUID.fromString(obj.getString("target")), + obj.getString("type") + ); + } + + @Override + public GetActivePunishmentMessage clone(GetActivePunishmentMessage value) { + return value; + } + }; + } + + @Override + 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 ? 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, 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 : gson.fromJson(obj.getString("reason"), PunishmentReason.class), + obj.getLong("expiresAt"), + tags + ); + } + + @Override + public GetActivePunishmentResponse clone(GetActivePunishmentResponse value) { + return value; + } + }; + } + + public record GetActivePunishmentMessage( + @NotNull UUID target, + @NotNull String type + ) {} + + public record GetActivePunishmentResponse( + boolean found, + @Nullable String type, + @Nullable String banId, + @Nullable PunishmentReason reason, + long expiresAt, + @NotNull List tags + ) {} +} 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..7e7ceb563 --- /dev/null +++ b/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/PunishPlayerProtocolObject.java @@ -0,0 +1,121 @@ +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 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 + 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("tags", new Gson().toJson(value.tags())); + 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")), + List.of(new Gson().fromJson(obj.getString("tags"), PunishmentTag[].class)), + 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 + } + }; + } + + public record PunishPlayerMessage( + @NotNull + UUID target, + @NotNull + String type, + @NotNull + PunishmentReason reason, + @NotNull + UUID staff, + List tags, + 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, + 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..6a09563f3 --- /dev/null +++ b/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/UnpunishPlayerProtocolObject.java @@ -0,0 +1,81 @@ +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()); + json.put("type", value.type()); + 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")), + obj.getString("type") + ); + } + + @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.isNull("errorMessage") ? null : obj.getString("errorMessage") + ); + } + + @Override + public UnpunishPlayerResponse clone(UnpunishPlayerResponse value) { + return value; + } + }; + } + + public record UnpunishPlayerMessage( + @NotNull UUID target, + @NotNull UUID staff, + @NotNull String type + ) {} + + public record UnpunishPlayerResponse( + boolean success, + @Nullable String errorMessage + ) {} +} 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/ActivePunishment.java b/commons/src/main/java/net/swofty/commons/punishment/ActivePunishment.java new file mode 100644 index 000000000..52fbde459 --- /dev/null +++ b/commons/src/main/java/net/swofty/commons/punishment/ActivePunishment.java @@ -0,0 +1,5 @@ +package net.swofty.commons.punishment; + +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/PunishmentId.java b/commons/src/main/java/net/swofty/commons/punishment/PunishmentId.java new file mode 100644 index 000000000..2cd6bc7f0 --- /dev/null +++ b/commons/src/main/java/net/swofty/commons/punishment/PunishmentId.java @@ -0,0 +1,16 @@ +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++) { + idBuilder.append(hexChars.charAt(RANDOM.nextInt(hexChars.length()))); + } + return new PunishmentId(idBuilder.toString()); + } +} 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..6dc73a046 --- /dev/null +++ b/commons/src/main/java/net/swofty/commons/punishment/PunishmentMessages.java @@ -0,0 +1,56 @@ +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(ActivePunishment punishment) { + long expiresAt = punishment.expiresAt(); + PunishmentReason reason = punishment.reason(); + String banId = punishment.banId(); + + 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"; + } + + 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(ActivePunishment punishment) { + long expiresAt = punishment.expiresAt(); + PunishmentReason reason = punishment.reason(); + + 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 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 + reasonLine + time + urlInfo + footer + line); + } +} 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..5b99ca61d --- /dev/null +++ b/commons/src/main/java/net/swofty/commons/punishment/PunishmentReason.java @@ -0,0 +1,35 @@ +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 org.jetbrains.annotations.Nullable; + +@Getter +@NoArgsConstructor +public class PunishmentReason { + @Nullable + private BanType banType; + @Nullable + private MuteType muteType; + + public PunishmentReason(@NonNull BanType banType) { + this.banType = banType; + } + + public PunishmentReason(@NonNull MuteType muteType) { + this.muteType = muteType; + } + + public String getReasonString() { + if (banType != null) { + return banType.getReason(); + } else if (muteType != null) { + return muteType.getReason(); + } else { + return "Could not resolve reason."; + } + } +} 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..fdd5a572a --- /dev/null +++ b/commons/src/main/java/net/swofty/commons/punishment/PunishmentRedis.java @@ -0,0 +1,141 @@ +package net.swofty.commons.punishment; + +import com.google.gson.Gson; +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.*; +import java.util.concurrent.CompletableFuture; + +public class PunishmentRedis { + 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; + + 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); + + 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(); + } + + 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 redisKey = key(playerId, type); + + HashMap data = new HashMap<>(Map.of( + "type", type, + "banId", id, + "reason", GSON.toJson(reason), + "expiresAt", String.valueOf(expiresAt) + )); + if (tags != null && !tags.isEmpty()) { + data.put("tags", GSON.toJson(tags)); + } + + jedis.hset(redisKey, data); + if (expiresAt > 0) { + long ttlSeconds = (expiresAt - System.currentTimeMillis()) / 1000; + if (ttlSeconds > 0) { + jedis.expire(redisKey, (int) ttlSeconds); + } + } else { + jedis.persist(redisKey); + } + } + } + + public static Optional getActive(UUID playerId, String type) { + if (!isInitialized()) return Optional.empty(); + + try (Jedis jedis = jedisPool.getResource()) { + String redisKey = key(playerId, type); + Map data = jedis.hgetAll(redisKey); + + if (data.isEmpty()) return Optional.empty(); + + String banId = data.get("banId"); + long expiresAt = Long.parseLong(data.getOrDefault("expiresAt", "-1")); + + if (expiresAt > 0 && System.currentTimeMillis() > expiresAt) { + jedis.del(redisKey); + return Optional.empty(); + } + + PunishmentReason reason = GSON.fromJson(data.get("reason"), PunishmentReason.class); + + List tags = List.of(); + String tagsJson = data.get("tags"); + if (tagsJson != null && !tagsJson.isBlank()) { + tags = List.of(GSON.fromJson(tagsJson, PunishmentTag[].class)); + } + + return Optional.of(new ActivePunishment(type, banId, reason, expiresAt, tags)); + } + } + + public static List getAllActive(UUID playerId) { + if (!isInitialized()) return List.of(); + + List result = new ArrayList<>(); + for (PunishmentType punishmentType : PunishmentType.values()) { + getActive(playerId, punishmentType.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)); + } + } +} 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..c95196e0c --- /dev/null +++ b/commons/src/main/java/net/swofty/commons/punishment/PunishmentTag.java @@ -0,0 +1,26 @@ +package net.swofty.commons.punishment; + +import lombok.Getter; + +@Getter +public enum PunishmentTag { + 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; + + PunishmentTag(String description, String shortCode) { + this.description = description; + this.shortCode = shortCode; + } + +} \ 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..50c4d634a --- /dev/null +++ b/commons/src/main/java/net/swofty/commons/punishment/template/BanType.java @@ -0,0 +1,82 @@ +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, + "https://www.hypixel.net/watchdog", true, true), + 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, 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, 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, + null, false, false, List.of("Boosting")), + 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, 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, 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, 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, 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 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 url, boolean preventRanked, boolean wipe) { + this(category, reason, shortName, weight, url, preventRanked, wipe, null, null); + } + + BanType(PunishmentCategory category, String reason, String shortName, int weight, + String url, boolean preventRanked, boolean wipe, List aliases) { + this(category, reason, shortName, weight, url, preventRanked, wipe, aliases, null); + } + + 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.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..af131c0e9 --- /dev/null +++ b/commons/src/main/java/net/swofty/commons/punishment/template/UnpunishReason.java @@ -0,0 +1,25 @@ +package net.swofty.commons.punishment.template; + +import lombok.Getter; + +@Getter +public enum UnpunishReason { + 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; + + UnpunishReason(String reason, String tag) { + this.reason = reason; + this.tag = tag; + } +} \ 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/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/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..c2cd546d3 --- /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.PunishmentService") +} + +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..d068ab929 --- /dev/null +++ b/service.punishment/src/main/java/net/swofty/service/punishment/ProxyRedis.java @@ -0,0 +1,75 @@ +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 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 { + 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 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(); + } +} \ 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..2d2fb5ddf --- /dev/null +++ b/service.punishment/src/main/java/net/swofty/service/punishment/PunishmentService.java @@ -0,0 +1,30 @@ +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.util.List; + +public class PunishmentService implements SkyBlockService { + + static void main(String[] args) { + 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/GetActivePunishmentEndpoint.java b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/GetActivePunishmentEndpoint.java new file mode 100644 index 000000000..225887560 --- /dev/null +++ b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/GetActivePunishmentEndpoint.java @@ -0,0 +1,39 @@ +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.ActivePunishment; +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 + { + + @Override + public ProtocolObject associatedProtocolObject() { + return new GetActivePunishmentProtocolObject(); + } + + @Override + public GetActivePunishmentProtocolObject.GetActivePunishmentResponse onMessage(ServiceProxyRequest message, GetActivePunishmentProtocolObject.GetActivePunishmentMessage messageObject) { + Optional existing = PunishmentRedis.getActive(messageObject.target(), messageObject.type()); + if (existing.isEmpty()) { + return new GetActivePunishmentProtocolObject.GetActivePunishmentResponse(false, null, null, null, 0, List.of()); + } + + ActivePunishment punishment = existing.get(); + return new GetActivePunishmentProtocolObject.GetActivePunishmentResponse( + true, + punishment.type(), + punishment.banId(), + punishment.reason(), + 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 new file mode 100644 index 000000000..8d7ea68f2 --- /dev/null +++ b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/PunishPlayerEndpoint.java @@ -0,0 +1,87 @@ +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; +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.Optional; + +public class PunishPlayerEndpoint implements ServiceEndpoint + { + + @Override + public ProtocolObject associatedProtocolObject() { + return new PunishPlayerProtocolObject(); + } + + @Override + public PunishPlayerProtocolObject.PunishPlayerResponse onMessage(ServiceProxyRequest message, PunishPlayerProtocolObject.PunishPlayerMessage messageObject) { + PunishmentType punishmentType; + try { + punishmentType = PunishmentType.valueOf(messageObject.type()); + } catch (IllegalArgumentException e) { + return new PunishPlayerProtocolObject.PunishPlayerResponse(false, null, PunishPlayerProtocolObject.ErrorCode.INVALID_TYPE, "The punishment type provided is invalid."); + } + + Instant now = 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."); + } + + boolean hasOverwriteTag = messageObject.tags() != null && messageObject.tags().contains(PunishmentTag.OVERWRITE); + if (!hasOverwriteTag) { + Optional existing = PunishmentRedis.getActive(messageObject.target(), messageObject.type()); + if (existing.isPresent()) { + return new PunishPlayerProtocolObject.PunishPlayerResponse(false, null, + PunishPlayerProtocolObject.ErrorCode.ALREADY_PUNISHED, existing.get().banId()); + } + } + + PunishmentReason reason = messageObject.reason(); + PunishmentId id = PunishmentId.generateId(); + + 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()) + .put("id", id.id()) + .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()) + .put("tags", messageObject.tags() != null ? gson.toJson(messageObject.tags()) : null) + ); + Logger.info("Issued {} punishment to {} for reason '{}' (expires at: {})", + messageObject.type(), + messageObject.target(), + reason.getReasonString(), + messageObject.expiresAt() + ); + return new PunishPlayerProtocolObject.PunishPlayerResponse(true, id.id(), null, 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 new file mode 100644 index 000000000..1479c8901 --- /dev/null +++ b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/UnpunishPlayerEndpoint.java @@ -0,0 +1,40 @@ +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.ActivePunishment; +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(), messageObject.type()); + if (existing.isEmpty()) { + return new UnpunishPlayerProtocolObject.UnpunishPlayerResponse(false, "No active " + messageObject.type().toLowerCase() + " found for this player."); + } + + 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."); + } + + Logger.info("Revoked {} for {} by staff {}", + messageObject.type(), messageObject.target(), messageObject.staff()); + return new UnpunishPlayerProtocolObject.UnpunishPlayerResponse(true, null); + } +} 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..e723fc9e9 100644 --- a/type.generic/build.gradle.kts +++ b/type.generic/build.gradle.kts @@ -26,7 +26,7 @@ dependencies { implementation("org.mongodb:bson:5.6.2") implementation("org.mongodb:mongodb-driver-sync:5.6.2") // 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..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 @@ -242,6 +242,7 @@ public void initialize(MinecraftServer server) { return player; }); } + } 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..7a53ce4fd --- /dev/null +++ b/type.generic/src/main/java/net/swofty/type/generic/command/commands/BanCommand.java @@ -0,0 +1,170 @@ +package net.swofty.type.generic.command.commands; + +import net.kyori.adventure.text.Component; +import net.minestom.server.command.builder.arguments.*; +import net.minestom.server.command.builder.suggestion.SuggestionEntry; +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.PunishmentTag; +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.HypixelPlayer; +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.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 = false +) +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()))); + } + }); + 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")))); + } + }); + + command.addSyntax((sender, context) -> { + HypixelPlayer player = (HypixelPlayer) sender; + String playerName = context.get(playerArg); + String duration = context.get(durationArg); + + 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 = 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) { + 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; + 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(player, net.minestom.server.utils.mojang.MojangUtils.getUUID(playerName), reason, + player.getUuid(), 0, -1, playerName, null); + } catch (IOException e) { + player.sendMessage("§cCould not find player: " + playerName); + } + }); + }, playerArg, reasonArg); + + command.addSyntax((sender, context) -> { + HypixelPlayer player = (HypixelPlayer) sender; + String playerName = context.get(playerArg); + + 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(player, net.minestom.server.utils.mojang.MojangUtils.getUUID(playerName), reason, + player.getUuid(), 0, -1, playerName, tags); + } catch (IOException e) { + player.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(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); + 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 + ); + + 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()); + } 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..1e5b76771 --- /dev/null +++ b/type.generic/src/main/java/net/swofty/type/generic/command/commands/MuteCommand.java @@ -0,0 +1,121 @@ +package net.swofty.type.generic.command.commands; + +import net.kyori.adventure.text.Component; +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.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.HypixelPlayer; +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; + +@CommandParameters( + aliases = "mute tempmute", + permission = Rank.STAFF, + description = "Mute a player from the server.", + usage = "/mute [duration] ", + allowsConsole = false +) +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) -> { + HypixelPlayer player = (HypixelPlayer) sender; + String playerName = context.get(playerArg); + String duration = context.get(durationArg); + + 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 = 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) { + 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; + 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(player, net.minestom.server.utils.mojang.MojangUtils.getUUID(playerName), reason, + player.getUuid(), 0, -1, playerName); + } catch (IOException e) { + player.sendMessage("§cCould not find player: " + playerName); + } + }); + }, playerArg, reasonArg); + } + + 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); + PunishPlayerProtocolObject.PunishPlayerMessage message = new PunishPlayerProtocolObject.PunishPlayerMessage( + targetUuid, + PunishmentType.MUTE.name(), + reason, + senderUuid, + List.of(), + 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 if (response.errorCode() == PunishPlayerProtocolObject.ErrorCode.ALREADY_PUNISHED) { + sender.sendMessage("§cThis player already has an active mute. Punishment ID: §7" + response.errorMessage()); + } 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..83b63449f --- /dev/null +++ b/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java @@ -0,0 +1,64 @@ +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 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; + +@CommandParameters( + description = "Unban a player from the server.", + usage = "/unban ", + aliases = "unban pardon unbanip pardonip", + permission = Rank.STAFF, + allowsConsole = false +) +public class UnBanCommand extends HypixelCommand { + + @Override + public void registerUsage(MinestomCommand command) { + Argument playerArg = ArgumentType.String("player"); + + command.addSyntax((sender, context) -> { + HypixelPlayer player = (HypixelPlayer) sender; + String playerName = context.get(playerArg); + + CompletableFuture.runAsync(() -> { + try { + UUID targetUuid = MojangUtils.getUUID(playerName); + ProxyService punishmentService = new ProxyService(ServiceType.PUNISHMENT); + UnpunishPlayerProtocolObject.UnpunishPlayerMessage message = new UnpunishPlayerProtocolObject.UnpunishPlayerMessage( + targetUuid, player.getUuid(), PunishmentType.BAN.name() + ); + + punishmentService.handleRequest(message).thenAccept(result -> { + if (result instanceof UnpunishPlayerProtocolObject.UnpunishPlayerResponse response) { + if (response.success()) { + player.sendMessage("§aSuccessfully unbanned player: " + playerName); + } else { + player.sendMessage("§c" + response.errorMessage()); + } + } + }).orTimeout(5, TimeUnit.SECONDS).exceptionally(_ -> { + player.sendMessage("§cCould not unban this player at this time. The punishment service may be offline."); + return null; + }); + } catch (IOException e) { + player.sendMessage("§cCould not find player: " + playerName); + } + }); + }, 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 new file mode 100644 index 000000000..43e3a8fa2 --- /dev/null +++ b/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnMuteCommand.java @@ -0,0 +1,64 @@ +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 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; + +@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) { + Argument playerArg = ArgumentType.String("player"); + + command.addSyntax((sender, context) -> { + HypixelPlayer player = (HypixelPlayer) sender; + String playerName = context.get(playerArg); + + CompletableFuture.runAsync(() -> { + try { + UUID targetUuid = MojangUtils.getUUID(playerName); + ProxyService punishmentService = new ProxyService(ServiceType.PUNISHMENT); + UnpunishPlayerProtocolObject.UnpunishPlayerMessage 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); + } + }); + }, 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 new file mode 100644 index 000000000..9dd79bc1f --- /dev/null +++ b/type.generic/src/main/java/net/swofty/type/generic/event/actions/ActionPlayerMute.java @@ -0,0 +1,38 @@ +package net.swofty.type.generic.event.actions; + +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.ActivePunishment; +import net.swofty.commons.punishment.PunishmentMessages; +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.concurrent.TimeUnit; + +public class ActionPlayerMute implements HypixelEventClass { + + @HypixelEvent(node = EventNodes.PLAYER, requireDataLoaded = false) + public void onPlayerChat(PlayerChatEvent event) { + Player player = event.getPlayer(); + try { + 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 muteResponse && muteResponse.found()) { + event.setCancelled(true); + 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 ac6f471e0..48c27ef79 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; @@ -35,10 +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.ActivePunishment; +import net.swofty.commons.punishment.PunishmentMessages; +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; @@ -69,6 +74,7 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; + import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -202,6 +208,8 @@ 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(); /** @@ -210,10 +218,46 @@ public void onProxyInitialization(ProxyInitializeEvent event) { GameManager.loopServers(server); } + private boolean checkPunished(Player player) { + try { + ProxyService service = new ProxyService(ServiceType.PUNISHMENT); + + CompletableFuture banFuture = service.handleRequest( + new GetActivePunishmentProtocolObject.GetActivePunishmentMessage(player.getUniqueId(), PunishmentType.BAN.name())); + CompletableFuture muteFuture = service.handleRequest( + new GetActivePunishmentProtocolObject.GetActivePunishmentMessage(player.getUniqueId(), PunishmentType.MUTE.name())); + + CompletableFuture.allOf(banFuture, muteFuture).orTimeout(3, TimeUnit.SECONDS).join(); + + 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; + } + + 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; + } catch (Exception e) { + return false; + } + } + @Subscribe - public void onPlayerJoin(PlayerChooseInitialServerEvent event) { + public EventTask onPlayerJoin(PlayerChooseInitialServerEvent event) { + return EventTask.async(() -> { Player player = event.getPlayer(); + if (checkPunished(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.") @@ -272,10 +316,15 @@ public void onPlayerJoin(PlayerChooseInitialServerEvent event) { FromProxyChannels.PROMPT_PLAYER_FOR_AUTHENTICATION, new JSONObject().put("uuid", player.getUniqueId().toString())); } + }); } @Subscribe public void onServerCrash(KickedFromServerEvent event) { + if (checkPunished(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..9a686fbb8 --- /dev/null +++ b/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerPunished.java @@ -0,0 +1,75 @@ +package net.swofty.velocity.redis.listeners; + +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.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.velocity.SkyBlockVelocity; +import net.swofty.velocity.redis.ChannelListener; +import net.swofty.velocity.redis.RedisListener; +import org.json.JSONObject; +import org.tinylog.Logger; + +import java.util.List; +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 { + throw new org.json.JSONException("Missing reason ban or reason_mute"); + } + } 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, 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.getReasonString())); + 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;