Repository navigation
GH-1428 Add punishment system #1429
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
077d5ef
6e35c6d
20ad6f4
535e885
b2e98bf
869bcb1
24e6465
f550305
2c7109c
9466fc7
57adbc6
215d511
ba78e0e
ee95629
67be256
82d1bff
7f63902
5cb0e90
11aa029
1d9fb7b
8c6abe8
16256f1
2099101
e54a449
c869dd2
ea90878
8f95859
d580c05
8430270
457ccf7
c228d0c
92a3e4c
d0d05b0
fafc37d
382485f
aca0021
d379bd1
4052a7d
660a851
60788bb
2b38a36
5cd6715
33c6f37
b7cc926
f759c75
1d6d5ff
10aabca
029714c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| services: | ||
| postgres: | ||
| image: postgres:16 | ||
| container_name: eternalcore-postgres | ||
| environment: | ||
| POSTGRES_DB: eternalcore | ||
| POSTGRES_USER: eternalcore | ||
| POSTGRES_PASSWORD: eternalcore | ||
| ports: | ||
| - "5432:5432" | ||
| volumes: | ||
| - eternalcore-postgres-data:/var/lib/postgresql/data | ||
|
|
||
| volumes: | ||
| eternalcore-postgres-data: |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| package com.eternalcode.core.feature.punishment; | ||
|
|
||
| import java.time.Instant; | ||
| import java.util.Optional; | ||
| import java.util.UUID; | ||
|
|
||
| /** | ||
| * Common read-only view of every punishment domain (ban, ip ban, mute, warn, kick). | ||
| * Status is derived, never stored: see {@link PunishmentStatus#resolve(Instant, Instant, Instant)}. | ||
| */ | ||
| public interface Punishment { | ||
|
|
||
| UUID id(); | ||
|
|
||
| PunishmentTarget target(); | ||
|
|
||
| PunishmentTarget operator(); | ||
|
|
||
| String reason(); | ||
|
|
||
| Instant createdAt(); | ||
|
|
||
| /** | ||
| * @return moment when punishment expires, null = permanent | ||
| */ | ||
| Instant expiresAt(); | ||
|
|
||
| /** | ||
| * @return revocation details, null = not revoked | ||
| */ | ||
| Revocation revocation(); | ||
|
|
||
| default Optional<Instant> expiresAtOptional() { | ||
| return Optional.ofNullable(this.expiresAt()); | ||
| } | ||
|
|
||
| default Optional<Revocation> revocationOptional() { | ||
| return Optional.ofNullable(this.revocation()); | ||
| } | ||
|
|
||
| default boolean isPermanent() { | ||
| return this.expiresAt() == null; | ||
| } | ||
|
|
||
| default PunishmentStatus status(Instant now) { | ||
| Revocation revocation = this.revocation(); | ||
| Instant revokedAt = revocation == null ? null : revocation.revokedAt(); | ||
|
|
||
| return PunishmentStatus.resolve(this.expiresAt(), revokedAt, now); | ||
| } | ||
|
|
||
| default PunishmentStatus status() { | ||
| return this.status(Instant.now()); | ||
| } | ||
|
|
||
| default boolean isActive(Instant now) { | ||
| return this.status(now) == PunishmentStatus.ACTIVE; | ||
| } | ||
|
|
||
| default boolean isActive() { | ||
| return this.isActive(Instant.now()); | ||
| } | ||
|
|
||
| /** | ||
| * Fail-fast validation shared by all punishment records. | ||
| */ | ||
| static void validate(Instant createdAt, Instant expiresAt, Revocation revocation) { | ||
| if (expiresAt != null && expiresAt.isBefore(createdAt)) { | ||
| throw new IllegalArgumentException("expiresAt cannot be before createdAt"); | ||
| } | ||
|
|
||
| if (revocation != null && revocation.revokedAt().isBefore(createdAt)) { | ||
| throw new IllegalArgumentException("revokedAt cannot be before createdAt"); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package com.eternalcode.core.feature.punishment; | ||
|
|
||
| import java.time.Instant; | ||
|
|
||
| public enum PunishmentStatus { | ||
|
|
||
| ACTIVE, | ||
| EXPIRED, | ||
| REVOKED, | ||
| INSTANT; | ||
|
|
||
| public static PunishmentStatus resolve(Instant expiresAt, Instant revokedAt, Instant now) { | ||
| boolean revokedBeforeExpiry = revokedAt != null && (expiresAt == null || revokedAt.isBefore(expiresAt)); | ||
|
|
||
| if (revokedBeforeExpiry) { | ||
| return REVOKED; | ||
| } | ||
|
|
||
| boolean expired = expiresAt != null && !now.isBefore(expiresAt); | ||
|
|
||
| if (expired) { | ||
| return EXPIRED; | ||
| } | ||
|
|
||
| return ACTIVE; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| package com.eternalcode.core.feature.punishment; | ||
|
|
||
| import java.util.UUID; | ||
| import org.bukkit.OfflinePlayer; | ||
| import org.bukkit.command.CommandSender; | ||
| import org.bukkit.entity.Player; | ||
|
|
||
| public record PunishmentTarget(UUID uuid, String name) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. w kilku miejscach uzywasz PunishmentTarget jednoczesnie jako targetu i operatora. Nazwa jest więc myląca.
Right now the API models console as a fake player using Please split this into something like: public record PunishmentTarget(UUID uuid, String name) {
}
public sealed interface PunishmentActor {
record Player(UUID uuid, String name) implements PunishmentActor {
}
record Console(String name) implements PunishmentActor {
}
}Then punishments should look like: PunishmentTarget target();
PunishmentActor operator();and This removes the fake console UUID, prevents invalid cases like using console as a punishment target, and makes the API reflect the actual domain model instead of overloading one type with two unrelated responsibilities. |
||
|
|
||
| public static final UUID CONSOLE_UUID = UUID.nameUUIDFromBytes("CONSOLE_UUID".getBytes()); | ||
|
|
||
| public static PunishmentTarget of(OfflinePlayer player) { | ||
| String name = player.getName(); | ||
|
|
||
| if (name == null) { | ||
| throw new IllegalArgumentException("Player " + player.getUniqueId() + " has no known name - resolve the player before punishing"); | ||
| } | ||
|
|
||
| return new PunishmentTarget(player.getUniqueId(), name); | ||
| } | ||
|
|
||
| public static PunishmentTarget of(Player player) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bukkit sam w sobie nie powinien przeciekac do modelu api, tak sadze. proponuje zebys tutaj robil to wszystko na uuid i w srodku cora tylko mapowal. |
||
| return new PunishmentTarget(player.getUniqueId(), player.getName()); | ||
| } | ||
|
|
||
| public static PunishmentTarget of(CommandSender sender) { | ||
| if (sender instanceof Player player) { | ||
| return of(player); | ||
| } | ||
|
|
||
| return new PunishmentTarget(CONSOLE_UUID, sender.getName()); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean equals(Object obj) { | ||
| if (this == obj) { | ||
| return true; | ||
| } | ||
|
|
||
| return obj instanceof PunishmentTarget other && this.uuid.equals(other.uuid); | ||
| } | ||
|
|
||
| @Override | ||
| public int hashCode() { | ||
| return this.uuid.hashCode(); | ||
| } | ||
| } | ||
|
Comment on lines
+34
to
+47
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. w jakim celu nadpisujesz te metody? |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package com.eternalcode.core.feature.punishment; | ||
|
|
||
| import java.time.Instant; | ||
|
|
||
| /** | ||
| * Describes who and when revoked a punishment (unban, unmute, ...). | ||
| */ | ||
| public record Revocation(PunishmentTarget revokedBy, Instant revokedAt) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package com.eternalcode.core.feature.punishment.ban; | ||
|
|
||
| import com.eternalcode.core.feature.punishment.Punishment; | ||
| import com.eternalcode.core.feature.punishment.PunishmentTarget; | ||
| import com.eternalcode.core.feature.punishment.Revocation; | ||
|
|
||
| import java.time.Instant; | ||
| import java.util.UUID; | ||
|
|
||
| /** | ||
| * Immutable ban of a single player account. | ||
| * | ||
| * @param expiresAt null = permanent | ||
| * @param revocation null = not revoked | ||
| */ | ||
| public record Ban( | ||
| UUID id, | ||
| PunishmentTarget target, | ||
| PunishmentTarget operator, | ||
| String reason, | ||
| Instant createdAt, | ||
| Instant expiresAt, | ||
| Revocation revocation | ||
| ) implements Punishment { | ||
|
|
||
| public Ban { | ||
| Punishment.validate(createdAt, expiresAt, revocation); | ||
| } | ||
|
|
||
| public static Ban issue(PunishmentTarget target, PunishmentTarget operator, String reason, Instant expiresAt) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Public API consumers should not be able to create punishment objects that look persisted but were never processed by EternalCore. Creation should go through the service. |
||
| return new Ban(UUID.randomUUID(), target, operator, reason, Instant.now(), expiresAt, null); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package com.eternalcode.core.feature.punishment.ban; | ||
|
|
||
| import com.eternalcode.core.feature.punishment.PunishmentTarget; | ||
|
|
||
| import net.kyori.adventure.text.Component; | ||
|
|
||
| import java.time.Instant; | ||
| import java.util.List; | ||
| import java.util.Optional; | ||
| import java.util.UUID; | ||
|
|
||
| public interface BanService { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Api do banów nie zadziała, ponieważ nic nie przekazujesz w EternalCoreApi interfejsie. nikt nie bedzie mial do zadnego serwisu dostepu. |
||
|
|
||
| /** | ||
| * Blocking, must not be called from the main thread. | ||
| */ | ||
| Ban ban(PunishmentTarget target, PunishmentTarget operator, String reason, Instant expiresAt, List<Component> kickMessage); | ||
|
|
||
| /** | ||
| * Blocking, must not be called from the main thread. | ||
| */ | ||
| void unban(PunishmentTarget target, PunishmentTarget operator); | ||
|
|
||
| boolean isBanned(UUID targetUuid); | ||
|
|
||
| Optional<Ban> getActiveBan(UUID targetUuid); | ||
|
|
||
| List<Ban> activeBans(); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| package com.eternalcode.core.feature.punishment.banip; | ||
|
|
||
| import com.eternalcode.core.feature.punishment.Punishment; | ||
| import com.eternalcode.core.feature.punishment.PunishmentTarget; | ||
| import com.eternalcode.core.feature.punishment.Revocation; | ||
|
|
||
| import java.time.Instant; | ||
| import java.util.UUID; | ||
|
|
||
| /** | ||
| * Immutable ban of an IP address. {@code target} is the player the ban was issued for. | ||
| * | ||
| * @param expiresAt null = permanent | ||
| * @param revocation null = not revoked | ||
| */ | ||
| public record BanIp( | ||
| UUID id, | ||
| String ip, | ||
| PunishmentTarget target, | ||
| PunishmentTarget operator, | ||
| String reason, | ||
| Instant createdAt, | ||
| Instant expiresAt, | ||
| Revocation revocation | ||
| ) implements Punishment { | ||
|
|
||
| public BanIp { | ||
| Punishment.validate(createdAt, expiresAt, revocation); | ||
| } | ||
|
|
||
| public static BanIp issue(String ip, PunishmentTarget target, PunishmentTarget operator, String reason, Instant expiresAt) { | ||
| return new BanIp(UUID.randomUUID(), ip, target, operator, reason, Instant.now(), expiresAt, null); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| package com.eternalcode.core.feature.punishment.banip; | ||
|
|
||
| import com.eternalcode.core.feature.punishment.PunishmentTarget; | ||
|
|
||
| import net.kyori.adventure.text.Component; | ||
|
|
||
| import java.time.Instant; | ||
| import java.util.List; | ||
| import java.util.Optional; | ||
|
|
||
| public interface BanIpService { | ||
|
|
||
| /** | ||
| * Blocking, must not be called from the main thread. | ||
| */ | ||
| BanIp banIp(String ip, PunishmentTarget target, PunishmentTarget operator, String reason, Instant expiresAt, List<Component> kickMessage); | ||
|
|
||
| /** | ||
| * Blocking, must not be called from the main thread. | ||
| */ | ||
| void unbanIp(String ip, PunishmentTarget operator); | ||
|
|
||
| boolean isIpBanned(String ip); | ||
|
|
||
| Optional<BanIp> getActiveIpBan(String ip); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| package com.eternalcode.core.feature.punishment.kick; | ||
|
|
||
| import com.eternalcode.core.feature.punishment.Punishment; | ||
| import com.eternalcode.core.feature.punishment.PunishmentStatus; | ||
| import com.eternalcode.core.feature.punishment.PunishmentTarget; | ||
| import com.eternalcode.core.feature.punishment.Revocation; | ||
|
|
||
| import java.time.Instant; | ||
| import java.util.UUID; | ||
|
|
||
| /** | ||
| * Immutable kick. Kick is instantaneous: it never expires and cannot be revoked, | ||
| * its status is always {@link PunishmentStatus#INSTANT}. | ||
| * | ||
| * @param massKick true when the kick was part of /kickall | ||
| */ | ||
| public record Kick( | ||
| UUID id, | ||
| PunishmentTarget target, | ||
| PunishmentTarget operator, | ||
| String reason, | ||
| Instant createdAt, | ||
| boolean massKick | ||
| ) implements Punishment { | ||
|
|
||
| public static Kick issue(PunishmentTarget target, PunishmentTarget operator, String reason, boolean massKick) { | ||
| return new Kick(UUID.randomUUID(), target, operator, reason, Instant.now(), massKick); | ||
| } | ||
|
|
||
| @Override | ||
| public Instant expiresAt() { | ||
| return null; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nie podoba mi sie zwracanie takie nulla tutaj, to oznacza ze wszystkie kicki sa pernamentne tak? a status jest instant. propozycja:: public interface Punishment {
UUID id();
PunishmentTarget target();
PunishmentActor operator();
String reason();
Instant createdAt();
}
public interface ExpirablePunishment extends Punishment {
Instant expiresAt();
Revocation revocation();
} |
||
| } | ||
|
|
||
| @Override | ||
| public Revocation revocation() { | ||
| return null; | ||
| } | ||
|
|
||
| @Override | ||
| public PunishmentStatus status(Instant now) { | ||
| return PunishmentStatus.INSTANT; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package com.eternalcode.core.feature.punishment.kick; | ||
|
|
||
| import com.eternalcode.core.feature.punishment.PunishmentTarget; | ||
|
|
||
| import net.kyori.adventure.text.Component; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public interface KickService { | ||
|
|
||
| /** | ||
| * Blocking, must not be called from the main thread. | ||
| */ | ||
| Kick kick(PunishmentTarget target, PunishmentTarget operator, String reason, List<Component> kickMessage, boolean massKick); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ignorujesz fakt, że
revokedAtmoże być później niż przekazany now. Przykład: kara cofnięta 10 stycznia, a pytamy o status 5 stycznia -> obecnie dostaniemy REVOKED, mimo że wtedy kara była jeszcze aktywna.