From 5c4bd8e53d0095592abfa3d537ddd508f2b28735 Mon Sep 17 00:00:00 2001 From: JvstvsHD <79066214+JvstvsHD@users.noreply.github.com> Date: Fri, 25 Oct 2024 16:12:10 +0200 Subject: [PATCH] Implement punishment logs --- .github/workflows/build.yml | 3 +- .github/workflows/publish.yml | 3 - .../api/duration/PunishmentDuration.java | 16 ++ .../necrify/api/punishment/Punishment.java | 9 ++ .../api/punishment/log/PunishmentLog.java | 3 +- .../punishment/log/PunishmentLogAction.java | 6 + .../punishment/log/PunishmentLogEntry.java | 40 +++-- .../necrify/api/user/NecrifyUser.java | 9 -- .../common/punishment/AbstractPunishment.java | 16 ++ .../AbstractTemporalPunishment.java | 2 +- .../punishment/log/NecrifyPunishmentLog.java | 137 ++++++++++++++++++ .../database/postgresql/1/patch_2.sql | 12 ++ .../necrify/velocity/user/VelocityUser.java | 6 +- 13 files changed, 228 insertions(+), 34 deletions(-) create mode 100644 necrify-common/src/main/java/de/jvstvshd/necrify/common/punishment/log/NecrifyPunishmentLog.java create mode 100644 necrify-common/src/main/resources/database/postgresql/1/patch_2.sql diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b43ef1e5..23834af2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,8 +1,7 @@ name: Build project and generate JavaDocs on: pull_request: - branches: - - "**" + types: [opened, reopened, edited] jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index da54c534..cc1b81d5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,13 +9,10 @@ name: Publish package to the Maven Central Repository on: - release: - types: [created] push: branches: - '**' - jobs: publish: runs-on: ubuntu-latest diff --git a/necrify-api/src/main/java/de/jvstvshd/necrify/api/duration/PunishmentDuration.java b/necrify-api/src/main/java/de/jvstvshd/necrify/api/duration/PunishmentDuration.java index 6085006d..6c2d6d88 100644 --- a/necrify-api/src/main/java/de/jvstvshd/necrify/api/duration/PunishmentDuration.java +++ b/necrify-api/src/main/java/de/jvstvshd/necrify/api/duration/PunishmentDuration.java @@ -18,6 +18,8 @@ package de.jvstvshd.necrify.api.duration; +import de.jvstvshd.necrify.api.punishment.Punishment; +import de.jvstvshd.necrify.api.punishment.TemporalPunishment; import org.jetbrains.annotations.ApiStatus; import java.sql.Timestamp; @@ -117,6 +119,20 @@ static PunishmentDuration fromDuration(Duration duration) { return rpd; } + /** + * Converts the given {@link Punishment} into a {@link PunishmentDuration}. If the punishment is a {@link TemporalPunishment}, + * the duration of the punishment will be returned. Otherwise, the duration will be permanent. + * @param punishment the punishment to convert + * @return the duration of the punishment or a permanent duration if this is a non-temporal punishment + */ + static PunishmentDuration ofPunishment(Punishment punishment) { + if (punishment instanceof TemporalPunishment temporalPunishment) { + return temporalPunishment.getDuration(); + } else { + return PermanentPunishmentDuration.PERMANENT; + } + } + /** * Whether this duration is permanent meaning the expiration should be 31.12.9999, 23:59:59. * diff --git a/necrify-api/src/main/java/de/jvstvshd/necrify/api/punishment/Punishment.java b/necrify-api/src/main/java/de/jvstvshd/necrify/api/punishment/Punishment.java index 41302f90..8ce4e9da 100644 --- a/necrify-api/src/main/java/de/jvstvshd/necrify/api/punishment/Punishment.java +++ b/necrify-api/src/main/java/de/jvstvshd/necrify/api/punishment/Punishment.java @@ -19,6 +19,7 @@ package de.jvstvshd.necrify.api.punishment; import de.jvstvshd.necrify.api.PunishmentException; +import de.jvstvshd.necrify.api.punishment.log.PunishmentLog; import de.jvstvshd.necrify.api.punishment.util.ReasonHolder; import de.jvstvshd.necrify.api.user.NecrifyUser; import net.kyori.adventure.text.Component; @@ -186,4 +187,12 @@ default Punishment getSuccessorOrNull() { */ @Nullable Punishment getPredecessor(); + + /** + * Loads the punishment log of this punishment. This will load all log entries that have been made on this punishment. + * @return a {@link CompletableFuture} containing the punishment log of this punishment + * @since 1.2.2 + */ + @NotNull + CompletableFuture loadPunishmentLog(); } diff --git a/necrify-api/src/main/java/de/jvstvshd/necrify/api/punishment/log/PunishmentLog.java b/necrify-api/src/main/java/de/jvstvshd/necrify/api/punishment/log/PunishmentLog.java index 151275ef..5c611da5 100644 --- a/necrify-api/src/main/java/de/jvstvshd/necrify/api/punishment/log/PunishmentLog.java +++ b/necrify-api/src/main/java/de/jvstvshd/necrify/api/punishment/log/PunishmentLog.java @@ -19,6 +19,7 @@ package de.jvstvshd.necrify.api.punishment.log; import de.jvstvshd.necrify.api.punishment.Punishment; +import de.jvstvshd.necrify.api.user.NecrifyUser; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -84,5 +85,5 @@ default PunishmentLogEntry getLatestEntry() { * @param action the action to log * @param message the message to log */ - void log(@NotNull PunishmentLogAction action, @NotNull String message); + void log(@NotNull PunishmentLogAction action, @NotNull String message, @NotNull NecrifyUser actor); } diff --git a/necrify-api/src/main/java/de/jvstvshd/necrify/api/punishment/log/PunishmentLogAction.java b/necrify-api/src/main/java/de/jvstvshd/necrify/api/punishment/log/PunishmentLogAction.java index 66c1388a..52504514 100644 --- a/necrify-api/src/main/java/de/jvstvshd/necrify/api/punishment/log/PunishmentLogAction.java +++ b/necrify-api/src/main/java/de/jvstvshd/necrify/api/punishment/log/PunishmentLogAction.java @@ -78,6 +78,12 @@ public interface PunishmentLogAction { */ PunishmentLogAction REMOVED = new SimplePunishmentLogAction("removed", true); + /** + * An unknown action was performed. This action is returned as default if the stored action type cannot be resolved to + * a proper type. This action can be logged multiple times. + */ + PunishmentLogAction UNKNOWN = new SimplePunishmentLogAction("unknown", false); + /** * A simple implementation of {@link PunishmentLogAction}. This class only contains the name and whether the action can only be logged once or more. * @param name the name of the action diff --git a/necrify-api/src/main/java/de/jvstvshd/necrify/api/punishment/log/PunishmentLogEntry.java b/necrify-api/src/main/java/de/jvstvshd/necrify/api/punishment/log/PunishmentLogEntry.java index ac758856..5c986e52 100644 --- a/necrify-api/src/main/java/de/jvstvshd/necrify/api/punishment/log/PunishmentLogEntry.java +++ b/necrify-api/src/main/java/de/jvstvshd/necrify/api/punishment/log/PunishmentLogEntry.java @@ -23,6 +23,7 @@ import de.jvstvshd.necrify.api.user.NecrifyUser; import net.kyori.adventure.text.Component; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.time.Instant; import java.util.Objects; @@ -30,24 +31,30 @@ /** * Represents an entry in a {@link PunishmentLog}. This contains all information about a punishment log entry. * There is no information about old values if they have been changed, this has to be done by using {@link #previous() the previous entry}. - * @param actor the actor who performed the action - * @param message the message of the action - * @param duration the duration of the punishment - * @param reason the reason of the punishment + * + * @param actor the actor who performed the action or null if the user does not exist anymore + * @param message the message of the action + * @param duration the duration of the punishment + * @param reason the reason of the punishment * @param predecessor the predecessor of the punishment or null if there is none - * @param punishment the punishment this entry belongs to - * @param successor the successor of the punishment or null if there is none - * @param action the action that was performed - * @param log the log this entry belongs to - * @param instant the instant the action was performed - * @param index the index of this entry in the log (0-based) + * @param punishment the punishment this entry belongs to + * @param successor the successor of the punishment or null if there is none + * @param action the action that was performed + * @param log the log this entry belongs to + * @param instant the instant the action was performed + * @param index the index of this entry in the log (0-based) + * @since 1.2.2 */ -public record PunishmentLogEntry(NecrifyUser actor, String message, PunishmentDuration duration, Component reason, - Punishment predecessor, Punishment punishment, Punishment successor, - PunishmentLogAction action, PunishmentLog log, Instant instant, int index) { +public record PunishmentLogEntry(@Nullable NecrifyUser actor, @Nullable String message, + @NotNull PunishmentDuration duration, @NotNull Component reason, + @Nullable Punishment predecessor, @NotNull Punishment punishment, + @Nullable Punishment successor, + @NotNull PunishmentLogAction action, @NotNull PunishmentLog log, + @NotNull Instant instant, int index) implements Comparable { /** * Returns the previous entry in the log. If this is the first entry, this entry is returned. + * * @return the previous entry in the log */ @NotNull @@ -57,6 +64,7 @@ public PunishmentLogEntry previous() { /** * Returns the next entry in the log. If this is the last entry, this entry is returned. + * * @return the next entry in the log */ @NotNull @@ -67,10 +75,16 @@ public PunishmentLogEntry next() { /** * Returns the affected user of the punishment. This is the user who is affected by the punishment and equivalent * to {@link #punishment()}.{@link Punishment#getUser() getUser()}. + * * @return the affected user of the punishment */ @NotNull public NecrifyUser getAffectedUser() { return punishment.getUser(); } + + @Override + public int compareTo(@NotNull PunishmentLogEntry o) { + return Integer.compare(index, o.index); + } } \ No newline at end of file diff --git a/necrify-api/src/main/java/de/jvstvshd/necrify/api/user/NecrifyUser.java b/necrify-api/src/main/java/de/jvstvshd/necrify/api/user/NecrifyUser.java index 6b0ecba9..f65d850a 100644 --- a/necrify-api/src/main/java/de/jvstvshd/necrify/api/user/NecrifyUser.java +++ b/necrify-api/src/main/java/de/jvstvshd/necrify/api/user/NecrifyUser.java @@ -202,13 +202,4 @@ default Optional getPunishment(@NotNull UUID punishmentUuid) { */ @NotNull Locale getLocale(); - - /** - * Loads the punishment log of this user. The punishment log contains all actions that were performed on this user. - * This method may take some time to complete. The returned future will complete exceptionally with {@link java.util.NoSuchElementException} - * if the user does not exist in the system. - * @return a {@link CompletableFuture} containing the punishment log of this user. - */ - @NotNull - CompletableFuture loadPunishmentLog(); } \ No newline at end of file diff --git a/necrify-common/src/main/java/de/jvstvshd/necrify/common/punishment/AbstractPunishment.java b/necrify-common/src/main/java/de/jvstvshd/necrify/common/punishment/AbstractPunishment.java index 6eff33cd..a15393f1 100644 --- a/necrify-common/src/main/java/de/jvstvshd/necrify/common/punishment/AbstractPunishment.java +++ b/necrify-common/src/main/java/de/jvstvshd/necrify/common/punishment/AbstractPunishment.java @@ -24,8 +24,11 @@ import de.jvstvshd.necrify.api.event.punishment.PunishmentPersecutedEvent; import de.jvstvshd.necrify.api.message.MessageProvider; import de.jvstvshd.necrify.api.punishment.Punishment; +import de.jvstvshd.necrify.api.punishment.log.PunishmentLog; import de.jvstvshd.necrify.api.user.NecrifyUser; import de.jvstvshd.necrify.common.AbstractNecrifyPlugin; +import de.jvstvshd.necrify.common.punishment.log.NecrifyPunishmentLog; +import de.jvstvshd.necrify.common.util.Util; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; import org.intellij.lang.annotations.Language; @@ -51,6 +54,7 @@ public abstract class AbstractPunishment implements Punishment { private final AbstractNecrifyPlugin plugin; private LocalDateTime creationTime; private Punishment successor; + private PunishmentLog cachedLog; @Language("sql") protected final static String APPLY_PUNISHMENT = "INSERT INTO necrify_punishment" + @@ -237,4 +241,16 @@ public boolean hasBeenCreated() { void setSuccessor0(Punishment successor) { this.successor = successor; } + + @Override + public @NotNull CompletableFuture loadPunishmentLog() { + if (cachedLog != null) { + return CompletableFuture.completedFuture(cachedLog); + } + return Util.executeAsync(() -> { + var log = new NecrifyPunishmentLog(this, plugin); + log.load(false); + return (cachedLog = log); + }, executor); + } } diff --git a/necrify-common/src/main/java/de/jvstvshd/necrify/common/punishment/AbstractTemporalPunishment.java b/necrify-common/src/main/java/de/jvstvshd/necrify/common/punishment/AbstractTemporalPunishment.java index c22f2614..0514d107 100644 --- a/necrify-common/src/main/java/de/jvstvshd/necrify/common/punishment/AbstractTemporalPunishment.java +++ b/necrify-common/src/main/java/de/jvstvshd/necrify/common/punishment/AbstractTemporalPunishment.java @@ -207,7 +207,7 @@ protected CompletableFuture applyPunishment() throws PunishmentExcep var total = temporalSuccessor.totalDuration(); successorNewExpiration = duration.expiration().plus(total.javaDuration()); } else { - successorNewExpiration = PunishmentDuration.permanent().expiration(); + successorNewExpiration = PunishmentDuration.PERMANENT.expiration(); } var issuanceSuccessor = duration.expiration(); Query.query(APPLY_TIMESTAMP_UPDATE) diff --git a/necrify-common/src/main/java/de/jvstvshd/necrify/common/punishment/log/NecrifyPunishmentLog.java b/necrify-common/src/main/java/de/jvstvshd/necrify/common/punishment/log/NecrifyPunishmentLog.java new file mode 100644 index 00000000..2e8a349e --- /dev/null +++ b/necrify-common/src/main/java/de/jvstvshd/necrify/common/punishment/log/NecrifyPunishmentLog.java @@ -0,0 +1,137 @@ +/* + * This file is part of Necrify (formerly Velocity Punishment), a plugin designed to manage player's punishments for the platforms Velocity and partly Paper. + * Copyright (C) 2022-2024 JvstvsHD + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.jvstvshd.necrify.common.punishment.log; + +import de.chojo.sadu.queries.api.call.Call; +import de.chojo.sadu.queries.api.query.Query; +import de.jvstvshd.necrify.api.duration.PunishmentDuration; +import de.jvstvshd.necrify.api.punishment.Punishment; +import de.jvstvshd.necrify.api.punishment.log.PunishmentLog; +import de.jvstvshd.necrify.api.punishment.log.PunishmentLogAction; +import de.jvstvshd.necrify.api.punishment.log.PunishmentLogActionRegistry; +import de.jvstvshd.necrify.api.punishment.log.PunishmentLogEntry; +import de.jvstvshd.necrify.api.user.NecrifyUser; +import de.jvstvshd.necrify.api.user.UserManager; +import de.jvstvshd.necrify.common.AbstractNecrifyPlugin; +import de.jvstvshd.necrify.common.io.Adapters; +import de.jvstvshd.necrify.common.util.Util; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.sql.Timestamp; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +public class NecrifyPunishmentLog implements PunishmentLog { + + private final Punishment punishment; + private final List entries = Collections.synchronizedList(new ArrayList<>()); + private final AbstractNecrifyPlugin plugin; + private final UserManager userManager; + + public NecrifyPunishmentLog(Punishment punishment, AbstractNecrifyPlugin plugin) { + this.punishment = punishment; + this.plugin = plugin; + this.userManager = plugin.getUserManager(); + } + + /** + * Loads the log from the database. This method should be called right after instantiation of this object, otherwise + * there will be no data contained in this object. + *

This method is ran synchronously and should not be called on the main thread. Use an asynchronous context yourself. + * + * @param loadIfAlreadyLoaded whether to load the log if it is already loaded + */ + public synchronized void load(boolean loadIfAlreadyLoaded) { + if (!loadIfAlreadyLoaded && !entries.isEmpty()) { + return; + } + var entries = Query.query("SELECT * FROM punishment_log WHERE punishment_id = ? ORDER BY id ASC") + .single(Call.of().bind(punishment.getUuid(), Adapters.UUID_ADAPTER)) + .map(row -> { + var id = row.getInt(1); + var actorUuid = row.getObject(2, UUID.class); + NecrifyUser actor; + if (actorUuid == null) { + actor = null; + } else { + actor = userManager.loadUser(actorUuid).join().orElseThrow(() -> new IllegalStateException("Actor not found.")); + } + var message = row.getString(3); + var duration = PunishmentDuration.fromTimestamp(row.getTimestamp(4)); + var reason = MiniMessage.miniMessage().deserialize(row.getString(5)); + var predecessor = plugin.getPunishment(row.getObject(6, UUID.class)).join().orElse(null); + var successor = plugin.getPunishment(row.getObject(7, UUID.class)).join().orElse(null); + var action = PunishmentLogActionRegistry.getAction(row.getString(8)).orElse(PunishmentLogAction.UNKNOWN); + var instant = row.getTimestamp(9).toInstant(); + return new PunishmentLogEntry(actor, message, duration, reason, predecessor, punishment, successor, action, this, instant, id); + }).all(); + Collections.sort(entries); + this.entries.clear(); + this.entries.addAll(entries); + } + + @Override + public @NotNull Punishment getPunishment() { + return punishment; + } + + @Override + public @NotNull List getEntries() { + return entries; + } + + @Override + public @NotNull List getEntries(@NotNull PunishmentLogAction action) { + return entries.stream().filter(entry -> entry.action().equals(action)).toList(); + } + + @Override + public @Nullable PunishmentLogEntry getEntry(@NotNull PunishmentLogAction action) { + return getEntries(action).getFirst(); + } + + @Override + public @NotNull PunishmentLogEntry getEntry(int index) { + return entries.get(index); + } + + @Override + public void log(@NotNull PunishmentLogAction action, @NotNull String message, @NotNull NecrifyUser actor) { + var entry = new PunishmentLogEntry(actor, message, PunishmentDuration.ofPunishment(punishment), punishment.getReason(), punishment.getPredecessor(), punishment, punishment.getSuccessor(), action, this, Instant.now(), entries.size()); + //insert at correct position (linked list) + entries.add(entry); + Util.executeAsync(() -> Query.query("INSERT INTO punishment_log (punishment_id, player_id, message, expiration, reason, predecessor, successor, action, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)") + .single(Call.of().bind(punishment.getUuid(), Adapters.UUID_ADAPTER) + .bind(actor.getUuid(), Adapters.UUID_ADAPTER) + .bind(message) + .bind(PunishmentDuration.ofPunishment(punishment).expirationAsTimestamp()) + .bind(MiniMessage.miniMessage().serialize(punishment.getReason())) + .bind(punishment.getPredecessor() == null ? null : punishment.getPredecessor().getUuid(), Adapters.UUID_ADAPTER) + .bind(punishment.getSuccessorOrNull() == null ? null : punishment.getSuccessor().getUuid(), Adapters.UUID_ADAPTER) + .bind(action.name()) + .bind(Timestamp.from(Instant.now()))) + .insert().changed(), plugin.getExecutor()); + + } +} diff --git a/necrify-common/src/main/resources/database/postgresql/1/patch_2.sql b/necrify-common/src/main/resources/database/postgresql/1/patch_2.sql new file mode 100644 index 00000000..fa97916c --- /dev/null +++ b/necrify-common/src/main/resources/database/postgresql/1/patch_2.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS punishment_log ( + id SERIAL PRIMARY KEY, + punishment_id UUID, + player_id UUID REFERENCES necrify_users (uuid) ON DELETE SET NULL, + message TEXT, + expiration TIMESTAMP, + reason TEXT, + predecessor UUID DEFAULT NULL, + successor UUID DEFAULT NULL, + action VARCHAR(128), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +) \ No newline at end of file diff --git a/necrify-velocity/src/main/java/de/jvstvshd/necrify/velocity/user/VelocityUser.java b/necrify-velocity/src/main/java/de/jvstvshd/necrify/velocity/user/VelocityUser.java index 7e44130b..d3468ecc 100644 --- a/necrify-velocity/src/main/java/de/jvstvshd/necrify/velocity/user/VelocityUser.java +++ b/necrify-velocity/src/main/java/de/jvstvshd/necrify/velocity/user/VelocityUser.java @@ -32,6 +32,7 @@ import de.jvstvshd.necrify.api.user.UserDeletionReason; import de.jvstvshd.necrify.common.io.Adapters; import de.jvstvshd.necrify.common.punishment.PunishmentBuilder; +import de.jvstvshd.necrify.common.punishment.log.NecrifyPunishmentLog; import de.jvstvshd.necrify.common.user.MojangAPI; import de.jvstvshd.necrify.common.util.Util; import de.jvstvshd.necrify.velocity.NecrifyVelocityPlugin; @@ -284,9 +285,4 @@ public CompletableFuture delete(@NotNull UserDeletionReason reason) { } return defaultLocale; } - - @Override - public @NotNull CompletableFuture loadPunishmentLog() { - throw new UnsupportedOperationException("Not implemented yet."); - } }