Add punishment details and history retrieval functionality

This commit introduces a new `DetailsComponent` for displaying detailed punishment data and establishes a route to view punishment history by ID and type. It also updates the API to support fetching individual punishment records and refines database mappings for improved data handling.
This commit is contained in:
Teriuihi 2025-04-19 04:02:51 +02:00
parent a1912b96d8
commit 3babde5513
22 changed files with 547 additions and 165 deletions

View File

@ -3,11 +3,11 @@ package com.alttd.altitudeweb.controllers.history;
import com.alttd.altitudeweb.api.HistoryApi; import com.alttd.altitudeweb.api.HistoryApi;
import com.alttd.altitudeweb.controllers.limits.RateLimit; import com.alttd.altitudeweb.controllers.limits.RateLimit;
import com.alttd.altitudeweb.model.HistoryCountDto; import com.alttd.altitudeweb.model.HistoryCountDto;
import com.alttd.altitudeweb.model.PunishmentHistoryListDto;
import com.alttd.altitudeweb.setup.Connection; import com.alttd.altitudeweb.setup.Connection;
import com.alttd.altitudeweb.database.Databases; import com.alttd.altitudeweb.database.Databases;
import com.alttd.altitudeweb.database.litebans.*; import com.alttd.altitudeweb.database.litebans.*;
import com.alttd.altitudeweb.model.PunishmentHistoryDto; import com.alttd.altitudeweb.model.PunishmentHistoryDto;
import com.alttd.altitudeweb.model.PunishmentHistoryInnerDto;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@ -23,15 +23,15 @@ import java.util.concurrent.TimeUnit;
public class HistoryApiController implements HistoryApi { public class HistoryApiController implements HistoryApi {
@Override @Override
public ResponseEntity<PunishmentHistoryDto> getHistoryForAll(String userType, String type, Integer page) { public ResponseEntity<PunishmentHistoryListDto> getHistoryForAll(String userType, String type, Integer page) {
return getHistoryForUsers(userType, type, "", page); return getHistoryForUsers(userType, type, "", page);
} }
@Override @Override
public ResponseEntity<PunishmentHistoryDto> getHistoryForUsers(String userType, String type, String user, Integer page) { public ResponseEntity<PunishmentHistoryListDto> getHistoryForUsers(String userType, String type, String user, Integer page) {
UserType userTypeEnum = UserType.getUserType(userType); UserType userTypeEnum = UserType.getUserType(userType);
HistoryType historyTypeEnum = HistoryType.getHistoryType(type); HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
PunishmentHistoryDto punishmentHistory = new PunishmentHistoryDto(); PunishmentHistoryListDto punishmentHistoryList = new PunishmentHistoryListDto();
CompletableFuture<List<HistoryRecord>> historyRecords = new CompletableFuture<>(); CompletableFuture<List<HistoryRecord>> historyRecords = new CompletableFuture<>();
Connection.getConnection(Databases.LITE_BANS) Connection.getConnection(Databases.LITE_BANS)
@ -46,14 +46,14 @@ public class HistoryApiController implements HistoryApi {
historyRecords.completeExceptionally(e); historyRecords.completeExceptionally(e);
} }
}); });
return mapPunishmentHistory(punishmentHistory, historyRecords); return mapPunishmentHistory(punishmentHistoryList, historyRecords);
} }
@Override @Override
public ResponseEntity<PunishmentHistoryDto> getHistoryForUuid(String userType, String type, String uuid, Integer page) { public ResponseEntity<PunishmentHistoryListDto> getHistoryForUuid(String userType, String type, String uuid, Integer page) {
UserType userTypeEnum = UserType.getUserType(userType); UserType userTypeEnum = UserType.getUserType(userType);
HistoryType historyTypeEnum = HistoryType.getHistoryType(type); HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
PunishmentHistoryDto punishmentHistory = new PunishmentHistoryDto(); PunishmentHistoryListDto punishmentHistoryList = new PunishmentHistoryListDto();
CompletableFuture<List<HistoryRecord>> historyRecords = new CompletableFuture<>(); CompletableFuture<List<HistoryRecord>> historyRecords = new CompletableFuture<>();
Connection.getConnection(Databases.LITE_BANS) Connection.getConnection(Databases.LITE_BANS)
@ -68,7 +68,7 @@ public class HistoryApiController implements HistoryApi {
historyRecords.completeExceptionally(e); historyRecords.completeExceptionally(e);
} }
}); });
return mapPunishmentHistory(punishmentHistory, historyRecords); return mapPunishmentHistory(punishmentHistoryList, historyRecords);
} }
@Override @Override
@ -153,6 +153,26 @@ public class HistoryApiController implements HistoryApi {
return ResponseEntity.ok().body(searchResultCountCompletableFuture.join()); return ResponseEntity.ok().body(searchResultCountCompletableFuture.join());
} }
@Override
public ResponseEntity<PunishmentHistoryDto> getHistoryById(String type, Integer id) {
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
CompletableFuture<HistoryRecord> historyRecordCompletableFuture = new CompletableFuture<>();
Connection.getConnection(Databases.LITE_BANS)
.runQuery(sqlSession -> {
log.debug("Loading history by id");
try {
HistoryRecord punishment = sqlSession.getMapper(IdHistoryMapper.class)
.getRecentHistory(historyTypeEnum, id);
historyRecordCompletableFuture.complete(punishment);
} catch (Exception e) {
log.error("Failed to load history count", e);
historyRecordCompletableFuture.completeExceptionally(e);
}
});
return ResponseEntity.ok().body(mapPunishmentHistory(historyRecordCompletableFuture.join()));
}
private ResponseEntity<HistoryCountDto> mapHistoryCount(HistoryCount historyCount) { private ResponseEntity<HistoryCountDto> mapHistoryCount(HistoryCount historyCount) {
HistoryCountDto historyCountDto = new HistoryCountDto(); HistoryCountDto historyCountDto = new HistoryCountDto();
historyCountDto.setBans(historyCount.getBans()); historyCountDto.setBans(historyCount.getBans());
@ -162,16 +182,23 @@ public class HistoryApiController implements HistoryApi {
return ResponseEntity.ok().body(historyCountDto); return ResponseEntity.ok().body(historyCountDto);
} }
private ResponseEntity<PunishmentHistoryDto> mapPunishmentHistory(PunishmentHistoryDto punishmentHistory, CompletableFuture<List<HistoryRecord>> historyRecords) { private ResponseEntity<PunishmentHistoryListDto> mapPunishmentHistory(PunishmentHistoryListDto punishmentHistoryList, CompletableFuture<List<HistoryRecord>> historyRecords) {
historyRecords.join().forEach(historyRecord -> { historyRecords.join().forEach(historyRecord -> {
PunishmentHistoryInnerDto.TypeEnum type = switch (historyRecord.getType().toLowerCase()) { PunishmentHistoryDto innerDto = mapPunishmentHistory(historyRecord);
case "ban" -> PunishmentHistoryInnerDto.TypeEnum.BAN; punishmentHistoryList.add(innerDto);
case "mute" -> PunishmentHistoryInnerDto.TypeEnum.MUTE; });
case "warn" -> PunishmentHistoryInnerDto.TypeEnum.WARN; return ResponseEntity.ok().body(punishmentHistoryList);
case "kick" -> PunishmentHistoryInnerDto.TypeEnum.KICK; }
private static PunishmentHistoryDto mapPunishmentHistory(HistoryRecord historyRecord) {
PunishmentHistoryDto.TypeEnum type = switch (historyRecord.getType().toLowerCase()) {
case "ban" -> PunishmentHistoryDto.TypeEnum.BAN;
case "mute" -> PunishmentHistoryDto.TypeEnum.MUTE;
case "warn" -> PunishmentHistoryDto.TypeEnum.WARN;
case "kick" -> PunishmentHistoryDto.TypeEnum.KICK;
default -> throw new IllegalStateException("Unexpected value: " + historyRecord.getType()); default -> throw new IllegalStateException("Unexpected value: " + historyRecord.getType());
}; };
PunishmentHistoryInnerDto innerDto = new PunishmentHistoryInnerDto() return new PunishmentHistoryDto()
.uuid(historyRecord.getUuid()) .uuid(historyRecord.getUuid())
.username(historyRecord.getPunishedName()) .username(historyRecord.getPunishedName())
.reason(historyRecord.getReason()) .reason(historyRecord.getReason())
@ -181,9 +208,7 @@ public class HistoryApiController implements HistoryApi {
.punishmentTime(historyRecord.getTime()) .punishmentTime(historyRecord.getTime())
.expiryTime(historyRecord.getUntil()) .expiryTime(historyRecord.getUntil())
.removedReason(historyRecord.getRemovedByReason()) .removedReason(historyRecord.getRemovedByReason())
.type(type); .type(type)
punishmentHistory.add(innerDto); .id(historyRecord.getId());
});
return ResponseEntity.ok().body(punishmentHistory);
} }
} }

View File

@ -16,6 +16,7 @@ dependencies {
implementation("org.mybatis:mybatis:3.5.13") implementation("org.mybatis:mybatis:3.5.13")
compileOnly("org.slf4j:slf4j-api:2.0.17") compileOnly("org.slf4j:slf4j-api:2.0.17")
compileOnly("org.slf4j:slf4j-simple:2.0.17") compileOnly("org.slf4j:slf4j-simple:2.0.17")
compileOnly("org.jetbrains:annotations:26.0.2")
} }
tasks.test { tasks.test {

View File

@ -5,6 +5,7 @@ import lombok.Data;
@Data @Data
public class HistoryRecord { public class HistoryRecord {
private String uuid; private String uuid;
private int id;
private String punishedName; private String punishedName;
private String reason; private String reason;
private String bannedByUuid; private String bannedByUuid;

View File

@ -0,0 +1,54 @@
package com.alttd.altitudeweb.database.litebans;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
public interface IdHistoryMapper {
@Results({
@Result(property = "id", column = "id"),
@Result(property = "uuid", column = "uuid"),
@Result(property = "punishedName", column = "punished_name"),
@Result(property = "reason", column = "reason"),
@Result(property = "bannedByUuid", column = "banned_by_uuid"),
@Result(property = "bannedByName", column = "banned_by_name"),
@Result(property = "removedByName", column = "removed_by_name"),
@Result(property = "time", column = "time"),
@Result(property = "until", column = "until"),
@Result(property = "removedByReason", column = "removed_by_reason")
})
@Select("""
SELECT punishment.id,
punishment.uuid,
user_lookup.name AS punished_name,
punishment.reason,
punishment.banned_by_uuid,
punishment.banned_by_name,
punishment.removed_by_name,
punishment.time,
punishment.until,
punishment.removed_by_reason
FROM (SELECT *
FROM ${table_name} AS punishment
WHERE punishment.id = #{id})
AS punishment
INNER JOIN user_lookup
ON user_lookup.uuid = punishment.uuid
""")
HistoryRecord getHistoryForId(@Param("table_name") String tableName,
@Param("type") String type,
@Param("id") int id);
default HistoryRecord getRecentHistory(HistoryType historyType, int id) {
HistoryRecord historyRecord = switch (historyType) {
case ALL -> throw new IllegalArgumentException("HistoryType.ALL is not supported");
case BAN -> getHistoryForId("litebans_bans", "ban", id);
case MUTE -> getHistoryForId("litebans_mutes", "mute", id);
case KICK -> getHistoryForId("litebans_kicks", "kick", id);
case WARN -> getHistoryForId("litebans_warnings", "warn", id);
};
historyRecord.setType(historyType.name().toLowerCase());
return historyRecord;
}
}

View File

@ -4,6 +4,7 @@ import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Result; import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results; import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Select;
import org.jetbrains.annotations.NotNull;
import java.util.List; import java.util.List;
@ -24,6 +25,7 @@ public interface NameHistoryMapper {
* order * order
*/ */
@Results({ @Results({
@Result(property = "id", column = "id"),
@Result(property = "uuid", column = "uuid"), @Result(property = "uuid", column = "uuid"),
@Result(property = "punishedName", column = "punished_name"), @Result(property = "punishedName", column = "punished_name"),
@Result(property = "reason", column = "reason"), @Result(property = "reason", column = "reason"),
@ -36,7 +38,8 @@ public interface NameHistoryMapper {
@Result(property = "type", column = "type") @Result(property = "type", column = "type")
}) })
@Select(""" @Select("""
SELECT all_punishments.uuid, SELECT id,
all_punishments.uuid,
user_lookup.name AS punished_name, user_lookup.name AS punished_name,
reason, reason,
banned_by_uuid, banned_by_uuid,
@ -62,7 +65,6 @@ public interface NameHistoryMapper {
* Retrieves a list of all types of recent punishment history records sorted * Retrieves a list of all types of recent punishment history records sorted
* in descending time order. This result does NOT contain kicks history * in descending time order. This result does NOT contain kicks history
* *
* @param nameColumn the column name in the database indicating the name to use for filtering
* @param limit the maximum number of records to fetch * @param limit the maximum number of records to fetch
* @param offset the starting offset position of the result set * @param offset the starting offset position of the result set
* *
@ -70,6 +72,7 @@ public interface NameHistoryMapper {
* order * order
*/ */
@Results({ @Results({
@Result(property = "id", column = "id"),
@Result(property = "uuid", column = "uuid"), @Result(property = "uuid", column = "uuid"),
@Result(property = "punishedName", column = "punished_name"), @Result(property = "punishedName", column = "punished_name"),
@Result(property = "reason", column = "reason"), @Result(property = "reason", column = "reason"),
@ -82,17 +85,19 @@ public interface NameHistoryMapper {
@Result(property = "type", column = "type") @Result(property = "type", column = "type")
}) })
@Select(""" @Select("""
SELECT punishments.uuid, SELECT punishment.id,
punishment.uuid,
user_lookup.name AS punished_name, user_lookup.name AS punished_name,
punishments.reason, punishment.reason,
punishments.banned_by_uuid, punishment.banned_by_uuid,
punishments.banned_by_name, punishment.banned_by_name,
punishments.removed_by_name, punishment.removed_by_name,
punishments.time, punishment.time,
punishments.until, punishment.until,
punishments.removed_by_reason, punishment.removed_by_reason,
punishments.type punishment.type
FROM (SELECT uuid, FROM (SELECT uuid,
id,
reason, reason,
banned_by_uuid, banned_by_uuid,
banned_by_name, banned_by_name,
@ -103,9 +108,9 @@ public interface NameHistoryMapper {
type type
FROM all_punishments FROM all_punishments
ORDER BY time DESC ORDER BY time DESC
LIMIT #{limit} OFFSET #{offset}) AS punishments LIMIT #{limit} OFFSET #{offset}) AS punishment
INNER JOIN user_lookup INNER JOIN user_lookup
ON user_lookup.uuid = punishments.uuid; ON user_lookup.uuid = punishment.uuid;
""") """)
List<HistoryRecord> getRecentAllHistory(@Param("limit") int limit, List<HistoryRecord> getRecentAllHistory(@Param("limit") int limit,
@Param("offset") int offset); @Param("offset") int offset);
@ -124,6 +129,7 @@ public interface NameHistoryMapper {
* order * order
*/ */
@Results({ @Results({
@Result(property = "id", column = "id"),
@Result(property = "uuid", column = "uuid"), @Result(property = "uuid", column = "uuid"),
@Result(property = "punishedName", column = "punished_name"), @Result(property = "punishedName", column = "punished_name"),
@Result(property = "reason", column = "reason"), @Result(property = "reason", column = "reason"),
@ -135,7 +141,8 @@ public interface NameHistoryMapper {
@Result(property = "removedByReason", column = "removed_by_reason") @Result(property = "removedByReason", column = "removed_by_reason")
}) })
@Select(""" @Select("""
SELECT punishment.uuid, SELECT punishment.id,
punishment.uuid,
user_lookup.name AS punished_name, user_lookup.name AS punished_name,
punishment.reason, punishment.reason,
punishment.banned_by_uuid, punishment.banned_by_uuid,
@ -162,8 +169,6 @@ public interface NameHistoryMapper {
* parameters. The records are sorted in descending order by time and limited to a specified range. * parameters. The records are sorted in descending order by time and limited to a specified range.
* *
* @param tableName the name of the database table to query the history records from * @param tableName the name of the database table to query the history records from
* @param partialName a partial or complete name of the user to filter the records, case-insensitive
* @param nameColumn the column name in the database indicating the name to use for filtering
* @param limit the maximum number of records to fetch * @param limit the maximum number of records to fetch
* @param offset the starting offset position of the result set * @param offset the starting offset position of the result set
* *
@ -171,6 +176,7 @@ public interface NameHistoryMapper {
* order * order
*/ */
@Results({ @Results({
@Result(property = "id", column = "id"),
@Result(property = "uuid", column = "uuid"), @Result(property = "uuid", column = "uuid"),
@Result(property = "punishedName", column = "punished_name"), @Result(property = "punishedName", column = "punished_name"),
@Result(property = "reason", column = "reason"), @Result(property = "reason", column = "reason"),
@ -182,7 +188,8 @@ public interface NameHistoryMapper {
@Result(property = "removedByReason", column = "removed_by_reason") @Result(property = "removedByReason", column = "removed_by_reason")
}) })
@Select(""" @Select("""
SELECT punishment.uuid, SELECT punishment.id,
punishment.uuid,
user_lookup.name AS punished_name, user_lookup.name AS punished_name,
punishment.reason, punishment.reason,
punishment.banned_by_uuid, punishment.banned_by_uuid,
@ -192,7 +199,8 @@ public interface NameHistoryMapper {
punishment.until, punishment.until,
punishment.removed_by_reason punishment.removed_by_reason
FROM ( FROM (
SELECT uuid, SELECT id,
uuid,
reason, reason,
banned_by_uuid, banned_by_uuid,
banned_by_name, banned_by_name,
@ -254,23 +262,23 @@ public interface NameHistoryMapper {
} }
} }
private List<HistoryRecord> getRecentBans(UserType userType, String partialName, int page) { private @NotNull List<HistoryRecord> getRecentBans(UserType userType, String partialName, int page) {
return addType(getRecent("litebans_bans", userType, partialName, page), "ban"); return addType(getRecent("litebans_bans", userType, partialName, page), "ban");
} }
private List<HistoryRecord> getRecentKicks(UserType userType, String partialName, int page) { private @NotNull List<HistoryRecord> getRecentKicks(UserType userType, String partialName, int page) {
return addType(getRecent("litebans_kicks", userType, partialName, page), "kick"); return addType(getRecent("litebans_kicks", userType, partialName, page), "kick");
} }
private List<HistoryRecord> getRecentMutes(UserType userType, String partialName, int page) { private @NotNull List<HistoryRecord> getRecentMutes(UserType userType, String partialName, int page) {
return addType(getRecent("litebans_mutes", userType, partialName, page), "mute"); return addType(getRecent("litebans_mutes", userType, partialName, page), "mute");
} }
private List<HistoryRecord> getRecentWarns(UserType userType, String partialName, int page) { private @NotNull List<HistoryRecord> getRecentWarns(UserType userType, String partialName, int page) {
return addType(getRecent("litebans_warnings", userType, partialName, page), "warn"); return addType(getRecent("litebans_warnings", userType, partialName, page), "warn");
} }
private List<HistoryRecord> addType(List<HistoryRecord> historyRecords, String type) { private @NotNull List<HistoryRecord> addType(@NotNull List<HistoryRecord> historyRecords, String type) {
historyRecords.forEach(historyRecord -> historyRecord.setType(type)); historyRecords.forEach(historyRecord -> historyRecord.setType(type));
return historyRecords; return historyRecords;
} }

View File

@ -1,10 +1,11 @@
package com.alttd.altitudeweb.database.litebans; package com.alttd.altitudeweb.database.litebans;
import com.alttd.altitudeweb.type_handler.UUIDTypeHandler;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Result; import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results; import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Select;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@ -14,10 +15,11 @@ public interface UUIDHistoryMapper {
int PAGE_SIZE = 10; int PAGE_SIZE = 10;
@Results({ @Results({
@Result(property = "uuid", column = "uuid", javaType = UUID.class, typeHandler = UUIDTypeHandler.class), @Result(property = "id", column = "id"),
@Result(property = "uuid", column = "uuid"),
@Result(property = "punishedName", column = "punished_name"), @Result(property = "punishedName", column = "punished_name"),
@Result(property = "reason", column = "reason"), @Result(property = "reason", column = "reason"),
@Result(property = "bannedByUuid", column = "banned_by_uuid", javaType = UUID.class, typeHandler = UUIDTypeHandler.class), @Result(property = "bannedByUuid", column = "banned_by_uuid"),
@Result(property = "bannedByName", column = "banned_by_name"), @Result(property = "bannedByName", column = "banned_by_name"),
@Result(property = "removedByName", column = "removed_by_name"), @Result(property = "removedByName", column = "removed_by_name"),
@Result(property = "time", column = "time"), @Result(property = "time", column = "time"),
@ -26,7 +28,7 @@ public interface UUIDHistoryMapper {
@Result(property = "type", column = "type") @Result(property = "type", column = "type")
}) })
@Select(""" @Select("""
SELECT all_punishments.uuid, user_lookup.name AS punished_name, reason, banned_by_uuid, banned_by_name, SELECT id, all_punishments.uuid, user_lookup.name AS punished_name, reason, banned_by_uuid, banned_by_name,
removed_by_name, time, until, removed_by_reason, type removed_by_name, time, until, removed_by_reason, type
FROM all_punishments FROM all_punishments
INNER JOIN user_lookup INNER JOIN user_lookup
@ -41,10 +43,11 @@ public interface UUIDHistoryMapper {
@Param("offset") int offset); @Param("offset") int offset);
@Results({ @Results({
@Result(property = "uuid", column = "uuid", javaType = UUID.class, typeHandler = UUIDTypeHandler.class), @Result(property = "id", column = "id"),
@Result(property = "uuid", column = "uuid"),
@Result(property = "punishedName", column = "punished_name"), @Result(property = "punishedName", column = "punished_name"),
@Result(property = "reason", column = "reason"), @Result(property = "reason", column = "reason"),
@Result(property = "bannedByUuid", column = "banned_by_uuid", javaType = UUID.class, typeHandler = UUIDTypeHandler.class), @Result(property = "bannedByUuid", column = "banned_by_uuid"),
@Result(property = "bannedByName", column = "banned_by_name"), @Result(property = "bannedByName", column = "banned_by_name"),
@Result(property = "removedByName", column = "removed_by_name"), @Result(property = "removedByName", column = "removed_by_name"),
@Result(property = "time", column = "time"), @Result(property = "time", column = "time"),
@ -52,7 +55,7 @@ public interface UUIDHistoryMapper {
@Result(property = "removedByReason", column = "removed_by_reason") @Result(property = "removedByReason", column = "removed_by_reason")
}) })
@Select(""" @Select("""
SELECT punishment.uuid, user_lookup.name AS punished_name, reason, banned_by_uuid, banned_by_name, SELECT punishment.id, punishment.uuid, user_lookup.name AS punished_name, reason, banned_by_uuid, banned_by_name,
removed_by_name, time, until, removed_by_reason removed_by_name, time, until, removed_by_reason
FROM ${tableName} AS punishment FROM ${tableName} AS punishment
INNER JOIN user_lookup ON user_lookup.uuid = punishment.uuid INNER JOIN user_lookup ON user_lookup.uuid = punishment.uuid
@ -66,7 +69,8 @@ public interface UUIDHistoryMapper {
@Param("limit") int limit, @Param("limit") int limit,
@Param("offset") int offset); @Param("offset") int offset);
default List<HistoryRecord> getRecent(HistoryType historyType, UserType userType, UUID uuid, int page) { default List<HistoryRecord> getRecent(@NotNull HistoryType historyType, @NotNull UserType userType,
@NotNull UUID uuid, int page) {
return switch (historyType) { return switch (historyType) {
case ALL -> getRecentAll(userType, uuid, page); case ALL -> getRecentAll(userType, uuid, page);
case BAN -> getRecentBans(userType, uuid, page); case BAN -> getRecentBans(userType, uuid, page);
@ -76,7 +80,8 @@ public interface UUIDHistoryMapper {
}; };
} }
private List<HistoryRecord> getRecent(String tableName, UserType userType, UUID uuid, int page) { private List<HistoryRecord> getRecent(@NotNull String tableName, @NotNull UserType userType,
@NotNull UUID uuid, int page) {
int offset = page * PAGE_SIZE; int offset = page * PAGE_SIZE;
int limit = PAGE_SIZE; int limit = PAGE_SIZE;
return switch (userType) { return switch (userType) {
@ -85,7 +90,7 @@ public interface UUIDHistoryMapper {
}; };
} }
private List<HistoryRecord> getRecentAll(UserType userType, UUID uuid, int page) { private List<HistoryRecord> getRecentAll(@NotNull UserType userType, @NotNull UUID uuid, int page) {
int offset = page * PAGE_SIZE; int offset = page * PAGE_SIZE;
int limit = PAGE_SIZE; int limit = PAGE_SIZE;
return switch (userType) { return switch (userType) {
@ -96,23 +101,24 @@ public interface UUIDHistoryMapper {
}; };
} }
private List<HistoryRecord> getRecentBans(UserType userType, UUID uuid, int page) { private @NotNull List<HistoryRecord> getRecentBans(@NotNull UserType userType, @NotNull UUID uuid, int page) {
return addType(getRecent("litebans_bans", userType, uuid, page), "ban"); return addType(getRecent("litebans_bans", userType, uuid, page), "ban");
} }
private List<HistoryRecord> getRecentKicks(UserType userType, UUID uuid, int page) { private @NotNull List<HistoryRecord> getRecentKicks(@NotNull UserType userType, @NotNull UUID uuid, int page) {
return addType(getRecent("litebans_kicks", userType, uuid, page), "kick"); return addType(getRecent("litebans_kicks", userType, uuid, page), "kick");
} }
private List<HistoryRecord> getRecentMutes(UserType userType, UUID uuid, int page) { private @NotNull List<HistoryRecord> getRecentMutes(@NotNull UserType userType, @NotNull UUID uuid, int page) {
return addType(getRecent("litebans_mutes", userType, uuid, page), "mute"); return addType(getRecent("litebans_mutes", userType, uuid, page), "mute");
} }
private List<HistoryRecord> getRecentWarns(UserType userType, UUID uuid, int page) { private @NotNull List<HistoryRecord> getRecentWarns(@NotNull UserType userType, @NotNull UUID uuid, int page) {
return addType(getRecent("litebans_warnings", userType, uuid, page), "warn"); return addType(getRecent("litebans_warnings", userType, uuid, page), "warn");
} }
private List<HistoryRecord> addType(List<HistoryRecord> historyRecords, String type) { @Contract("_, _ -> param1")
private @NotNull List<HistoryRecord> addType(@NotNull List<HistoryRecord> historyRecords, @NotNull String type) {
historyRecords.forEach(historyRecord -> historyRecord.setType(type)); historyRecords.forEach(historyRecord -> historyRecord.setType(type));
return historyRecords; return historyRecords;
} }

View File

@ -18,6 +18,7 @@ public class InitializeLiteBans {
configuration.addMapper(NameHistoryMapper.class); configuration.addMapper(NameHistoryMapper.class);
configuration.addMapper(UUIDHistoryMapper.class); configuration.addMapper(UUIDHistoryMapper.class);
configuration.addMapper(HistoryCountMapper.class); configuration.addMapper(HistoryCountMapper.class);
configuration.addMapper(IdHistoryMapper.class);
}).join() }).join()
.runQuery(sqlSession -> { .runQuery(sqlSession -> {
createAllPunishmentsView(sqlSession); createAllPunishmentsView(sqlSession);
@ -29,15 +30,15 @@ public class InitializeLiteBans {
private static void createAllPunishmentsView(SqlSession sqlSession) { private static void createAllPunishmentsView(SqlSession sqlSession) {
String query = """ String query = """
CREATE VIEW IF NOT EXISTS all_punishments AS CREATE VIEW IF NOT EXISTS all_punishments AS
SELECT uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason, SELECT id, uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason,
'ban' as type 'ban' as type
FROM litebans_bans FROM litebans_bans
UNION ALL UNION ALL
SELECT uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason, SELECT id, uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason,
'mute' as type 'mute' as type
FROM litebans_mutes FROM litebans_mutes
UNION ALL UNION ALL
SELECT uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason, SELECT id, uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason,
'warn' as type 'warn' as type
FROM litebans_warnings FROM litebans_warnings
ORDER BY time DESC; ORDER BY time DESC;

View File

@ -45,6 +45,10 @@ export const routes: Routes = [
path: 'bans', path: 'bans',
loadComponent: () => import('./bans/bans.component').then(m => m.BansComponent) loadComponent: () => import('./bans/bans.component').then(m => m.BansComponent)
}, },
{
path: 'bans/:type/:id',
loadComponent: () => import('./bans/details/details.component').then(m => m.DetailsComponent)
},
{ {
path: 'economy', path: 'economy',
loadComponent: () => import('./economy/economy.component').then(m => m.EconomyComponent) loadComponent: () => import('./economy/economy.component').then(m => m.EconomyComponent)

View File

@ -57,7 +57,8 @@
</div> </div>
<div class="historyTable"> <div class="historyTable">
<app-history [userType]="userType" [punishmentType]="punishmentType" <app-history [userType]="userType" [punishmentType]="punishmentType"
[page]="page" [searchTerm]="finalSearchTerm" (pageChange)="updatePageSize($event)"> [page]="page" [searchTerm]="finalSearchTerm" (pageChange)="updatePageSize($event)"
(selectItem)="setSearch($event)">
</app-history> </app-history>
</div> </div>
<div class="changePageButtons"> <div class="changePageButtons">

View File

@ -5,6 +5,7 @@ import {HistoryCount, HistoryService} from '../../api';
import {NgClass, NgForOf, NgIf} from '@angular/common'; import {NgClass, NgForOf, NgIf} from '@angular/common';
import {FormsModule} from '@angular/forms'; import {FormsModule} from '@angular/forms';
import {catchError, map, Observable} from 'rxjs'; import {catchError, map, Observable} from 'rxjs';
import {SearchParams} from './search-terms';
@Component({ @Component({
selector: 'app-bans', selector: 'app-bans',
@ -210,4 +211,11 @@ export class BansComponent implements OnInit {
this.actualPage = this.page; this.actualPage = this.page;
} }
} }
public setSearch(searchParams: SearchParams) {
this.userType = searchParams.userType
this.searchTerm = searchParams.searchTerm
this.punishmentType = searchParams.punishmentType
this.search();
}
} }

View File

@ -0,0 +1,62 @@
<ng-container>
<app-header [current_page]="'bans'" height="200px" background_image="/public/img/backgrounds/staff.png"
[overlay_gradient]="0.5">>
<div class="title" header-content>
<h1>Minecraft Punishments</h1>
</div>
</app-header>
<ng-container *ngIf="punishment === undefined">
<p>Loading...</p>
</ng-container>
<ng-container *ngIf="punishment">
<table [cellSpacing]="0">
<div>
<p>type: {{ this.historyFormat.getType(punishment) }}</p>
<p>is active: {{ this.historyFormat.isActive(punishment) }}</p>
<tbody>
<tr>
<td>Player</td>
<td>
<div class="playerContainer">
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(punishment.uuid)" width="25" height="25"
alt="{{punishment.username}}'s Minecraft skin">
<span class="username">{{ punishment.username }}</span>
</div>
</td>
</tr>
<tr>
<td>Moderator</td>
<td>
<div class="playerContainer">
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(punishment.punishedByUuid)" width="25"
height="25"
alt="{{punishment.punishedBy}}'s Minecraft skin">
<span class="username">{{ punishment.punishedBy }}</span>
</div>
</td>
</tr>
<tr>
<td>Reason</td>
<td>{{ punishment.reason | removeTrailingPeriod }}</td>
</tr>
<tr>
<td>Date</td>
<td>{{ this.historyFormat.getPunishmentTime(punishment) }}</td>
</tr>
<tr>
<td>Expires</td>
<td>{{ this.historyFormat.getExpiredTime(punishment) }}</td>
</tr>
<ng-container *ngIf="punishment.removedBy !== undefined && punishment.removedBy.length > 0">
<tr>
<td>Un{{ this.historyFormat.getType(punishment).toLocaleLowerCase() }} reason</td>
<td>{{ punishment.removedReason == null ? 'No reason specified' : punishment.removedReason }}</td>
</tr>
</ng-container>
</tbody>
</div>
</table>
</ng-container>
</ng-container>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DetailsComponent } from './details.component';
describe('DetailsComponent', () => {
let component: DetailsComponent;
let fixture: ComponentFixture<DetailsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DetailsComponent]
})
.compileComponents();
fixture = TestBed.createComponent(DetailsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,69 @@
import {Component, OnInit} from '@angular/core';
import {HistoryService, PunishmentHistory} from '../../../api';
import {NgIf, NgOptimizedImage} from '@angular/common';
import {RemoveTrailingPeriodPipe} from '../../util/RemoveTrailingPeriodPipe';
import {HistoryFormatService} from '../history-format.service';
import {ActivatedRoute} from '@angular/router';
import {catchError, map} from 'rxjs';
import {HeaderComponent} from '../../header/header.component';
@Component({
selector: 'app-details',
imports: [
NgIf,
NgOptimizedImage,
RemoveTrailingPeriodPipe,
HeaderComponent
],
templateUrl: './details.component.html',
styleUrl: './details.component.scss'
})
export class DetailsComponent implements OnInit {
type: 'all' | 'ban' | 'mute' | 'kick' | 'warn' = 'all';
id: number = -1;
punishment: PunishmentHistory | undefined;
constructor(private route: ActivatedRoute,
private historyApi: HistoryService,
public historyFormat: HistoryFormatService) {
}
ngOnInit(): void {
this.route.paramMap.subscribe(params => {
switch (params.get('type')) {
case 'ban':
this.type = 'ban';
break;
case 'mute':
this.type = 'mute';
break;
case 'kick':
this.type = 'kick';
break;
case 'warn':
this.type = 'warn';
break;
default:
throw new Error("Invalid type");
}
this.id = Number(params.get('id') || '-1');
this.loadPunishmentData();
});
}
private loadPunishmentData() {
if (!this.type || this.type === 'all' || this.id < 0 || isNaN(this.id)) {
console.error("Invalid type or id");
return;
}
this.historyApi.getHistoryById(this.type, this.id).pipe(
map(punishment => {
this.punishment = punishment;
}),
catchError(err => {
console.error(err);
return [];
})
).subscribe();
}
}

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { HistoryFormatService } from './history-format.service';
describe('HistoryFormatService', () => {
let service: HistoryFormatService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(HistoryFormatService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,58 @@
import {Injectable} from '@angular/core';
import {PunishmentHistory} from '../../api';
@Injectable({
providedIn: 'root'
})
export class HistoryFormatService {
constructor() {
}
public getPunishmentTime(entry: PunishmentHistory) {
const date = new Date(entry.punishmentTime);
return date.toLocaleString(navigator.language, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false
});
}
public isActive(entry: PunishmentHistory): boolean {
return entry.expiryTime > Date.now();
}
public getType(entry: PunishmentHistory): string {
return entry.type.charAt(0).toUpperCase() + entry.type.slice(1);
}
public getExpiredTime(entry: PunishmentHistory): string {
let suffix: string = '';
if (entry.removedBy !== null && entry.removedBy !== undefined) {
suffix = '(' + entry.removedBy + ')';
}
if (entry.expiryTime <= 0) {
return "Permanent " + this.getType(entry) + " " + suffix;
}
const date = new Date(entry.expiryTime);
return date.toLocaleString(navigator.language, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false
}) + " " + suffix;
}
public getAvatarUrl(entry: string): string {
let uuid = entry.replace('-', '');
if (uuid === 'C') {
uuid = "f78a4d8dd51b4b3998a3230f2de0c670"
}
return `https://crafatar.com/avatars/${uuid}?size=25&overlay`;
}
}

View File

@ -19,24 +19,32 @@
<div> <div>
<tbody> <tbody>
<tr class="historyPlayerRow" *ngFor="let entry of history"> <tr class="historyPlayerRow" *ngFor="let entry of history">
<td class="historyType">{{ getType(entry) }}</td> <td class="historyType" (click)="showDetailedPunishment(entry)">
<td class="historyPlayer"> {{ this.historyFormat.getType(entry) }}
</td>
<td class="historyPlayer" (click)="setSearch(entry.type, entry.username, 'player')">
<div class="playerContainer"> <div class="playerContainer">
<img class="avatar" [ngSrc]="getAvatarUrl(entry.uuid)" width="25" height="25" <img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(entry.uuid)" width="25" height="25"
alt="{{entry.username}}'s Minecraft skin"> alt="{{entry.username}}'s Minecraft skin">
<span class="username">{{ entry.username }}</span> <span class="username">{{ entry.username }}</span>
</div> </div>
</td> </td>
<td class="historyPlayer"> <td class="historyPlayer" (click)="setSearch(entry.type, entry.punishedBy, 'staff')">
<div class="playerContainer"> <div class="playerContainer">
<img class="avatar" [ngSrc]="getAvatarUrl(entry.punishedByUuid)" width="25" height="25" <img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(entry.punishedByUuid)" width="25" height="25"
alt="{{entry.punishedBy}}'s Minecraft skin"> alt="{{entry.punishedBy}}'s Minecraft skin">
<span>{{ entry.punishedBy }}</span> <span>{{ entry.punishedBy }}</span>
</div> </div>
</td> </td>
<td class="historyReason">{{ entry.reason | removeTrailingPeriod }}</td> <td class="historyReason" (click)="showDetailedPunishment(entry)">
<td class="historyDate">{{ getPunishmentTime(entry) }}</td> {{ entry.reason | removeTrailingPeriod }}
<td class="historyDate">{{ getExpiredTime(entry) }}</td> </td>
<td class="historyDate" (click)="showDetailedPunishment(entry)">
{{ this.historyFormat.getPunishmentTime(entry) }}
</td>
<td class="historyDate" (click)="showDetailedPunishment(entry)">
{{ this.historyFormat.getExpiredTime(entry) }}
</td>
</tr> </tr>
</tbody> </tbody>
</div> </div>

View File

@ -62,6 +62,7 @@ table tr td {
.historyPlayerRow:hover { .historyPlayerRow:hover {
background-color: var(--history-table-row-color); background-color: var(--history-table-row-color);
cursor: pointer;
} }
.historyTableHead { .historyTableHead {

View File

@ -1,11 +1,14 @@
import {Component, EventEmitter, Input, OnChanges, OnInit, Output} from '@angular/core'; import {Component, EventEmitter, Input, OnChanges, OnInit, Output} from '@angular/core';
import {BASE_PATH, HistoryService, PunishmentHistoryInner} from '../../../api'; import {BASE_PATH, HistoryService, PunishmentHistory} from '../../../api';
import {catchError, map, Observable, shareReplay} from 'rxjs'; import {catchError, map, Observable, shareReplay} from 'rxjs';
import {NgForOf, NgIf, NgOptimizedImage} from '@angular/common'; import {NgForOf, NgIf, NgOptimizedImage} from '@angular/common';
import {CookieService} from 'ngx-cookie-service'; import {CookieService} from 'ngx-cookie-service';
import {RemoveTrailingPeriodPipe} from '../../util/RemoveTrailingPeriodPipe'; import {RemoveTrailingPeriodPipe} from '../../util/RemoveTrailingPeriodPipe';
import {HttpErrorResponse} from '@angular/common/http'; import {HttpErrorResponse} from '@angular/common/http';
import {environment} from '../../../environments/environment'; import {environment} from '../../../environments/environment';
import {HistoryFormatService} from '../history-format.service';
import {SearchParams} from '../search-terms';
import {Router} from '@angular/router';
@Component({ @Component({
selector: 'app-history', selector: 'app-history',
@ -30,12 +33,13 @@ export class HistoryComponent implements OnInit, OnChanges {
@Input() searchTerm: string = ''; @Input() searchTerm: string = '';
@Output() pageChange = new EventEmitter<number>(); @Output() pageChange = new EventEmitter<number>();
@Output() selectItem = new EventEmitter<SearchParams>();
private uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; private uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
public history: PunishmentHistoryInner[] = [] public history: PunishmentHistory[] = []
constructor(private historyApi: HistoryService) { constructor(private historyApi: HistoryService, public historyFormat: HistoryFormatService, private router: Router) {
} }
ngOnChanges(): void { ngOnChanges(): void {
@ -47,7 +51,7 @@ export class HistoryComponent implements OnInit, OnChanges {
} }
private reloadHistory(): void { private reloadHistory(): void {
let historyObservable: Observable<PunishmentHistoryInner[]>; let historyObservable: Observable<PunishmentHistory[]>;
if (this.searchTerm.length === 0) { if (this.searchTerm.length === 0) {
historyObservable = this.historyApi.getHistoryForAll(this.userType, this.punishmentType, this.page); historyObservable = this.historyApi.getHistoryForAll(this.userType, this.punishmentType, this.page);
} else { } else {
@ -89,42 +93,31 @@ export class HistoryComponent implements OnInit, OnChanges {
).subscribe(); ).subscribe();
} }
public getPunishmentTime(entry: PunishmentHistoryInner) { setSearch(type: PunishmentHistory.TypeEnum, name: string, userType: 'player' | 'staff') {
const date = new Date(entry.punishmentTime); let punishmentType: 'all' | 'ban' | 'mute' | 'kick' | 'warn' = 'all';
return date.toLocaleString(navigator.language, { switch (type) {
year: 'numeric', case 'ban':
month: 'short', punishmentType = 'ban';
day: 'numeric', break;
hour: '2-digit', case 'mute':
minute: '2-digit', punishmentType = 'mute';
hour12: false break;
}); case 'kick':
punishmentType = 'kick';
break;
case 'warn':
punishmentType = 'warn';
break;
}
let searchParams: SearchParams = {
userType: userType,
punishmentType: punishmentType,
searchTerm: name
}
this.selectItem.emit(searchParams);
} }
public getType(entry: PunishmentHistoryInner) { public showDetailedPunishment(entry: PunishmentHistory) {
return entry.type.charAt(0).toUpperCase() + entry.type.slice(1); this.router.navigate([`bans/${entry.type}/${entry.id}`]).then();
}
public getExpiredTime(entry: PunishmentHistoryInner) {
if (entry.expiryTime <= 0) {
return "Permanent " + this.getType(entry);
}
const date = new Date(entry.expiryTime);
return date.toLocaleString(navigator.language, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false
});
}
public getAvatarUrl(entry: string): string {
let uuid = entry.replace('-', '');
if (uuid === 'C') {
uuid = "f78a4d8dd51b4b3998a3230f2de0c670"
}
return `https://crafatar.com/avatars/${uuid}?size=25&overlay`;
} }
} }

View File

@ -0,0 +1,5 @@
export interface SearchParams {
userType: 'player' | 'staff';
searchTerm: string;
punishmentType: 'all' | 'ban' | 'mute' | 'kick' | 'warn';
}

View File

@ -26,6 +26,8 @@ paths:
$ref: './schemas/bans/bans.yml#/getTotalResultsForUuidSearch' $ref: './schemas/bans/bans.yml#/getTotalResultsForUuidSearch'
/history/{userType}/search-results/user/{type}/{user}: /history/{userType}/search-results/user/{type}/{user}:
$ref: './schemas/bans/bans.yml#/getTotalResultsForUserSearch' $ref: './schemas/bans/bans.yml#/getTotalResultsForUserSearch'
/history/single/{type}/{id}:
$ref: './schemas/bans/bans.yml#/getHistoryById'
/history/total: /history/total:
$ref: './schemas/bans/bans.yml#/getTotalPunishments' $ref: './schemas/bans/bans.yml#/getTotalPunishments'
/appeal/update-mail: /appeal/update-mail:

View File

@ -44,7 +44,7 @@ getHistoryForUsers:
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/PunishmentHistory' $ref: '#/components/schemas/PunishmentHistoryList'
default: default:
description: Unexpected error description: Unexpected error
content: content:
@ -70,7 +70,7 @@ getHistoryForAll:
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/PunishmentHistory' $ref: '#/components/schemas/PunishmentHistoryList'
default: default:
description: Unexpected error description: Unexpected error
content: content:
@ -97,7 +97,7 @@ getHistoryForUuid:
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/PunishmentHistory' $ref: '#/components/schemas/PunishmentHistoryList'
default: default:
description: Unexpected error description: Unexpected error
content: content:
@ -172,6 +172,29 @@ getTotalResultsForUuidSearch:
application/json: application/json:
schema: schema:
$ref: '../generic/errors.yml#/components/schemas/ApiError' $ref: '../generic/errors.yml#/components/schemas/ApiError'
getHistoryById:
get:
tags:
- history
summary: Gets history for specified id
description: Retrieves a specific history record
operationId: getHistoryById
parameters:
- $ref: '#/components/parameters/HistoryType'
- $ref: '#/components/parameters/Id'
responses:
'200':
description: Successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/PunishmentHistory'
default:
description: Unexpected error
content:
application/json:
schema:
$ref: '../generic/errors.yml#/components/schemas/ApiError'
components: components:
parameters: parameters:
HistoryType: HistoryType:
@ -211,13 +234,22 @@ components:
schema: schema:
type: integer type: integer
description: The page that should be retrieved description: The page that should be retrieved
Id:
name: id
in: path
required: true
schema:
type: integer
description: The id of the punishment that should be retrieved
schemas: schemas:
SearchResults: SearchResults:
type: integer type: integer
description: A number representing the total count of results for the search query description: A number representing the total count of results for the search query
PunishmentHistory: PunishmentHistoryList:
type: array type: array
items: items:
$ref: '#/components/schemas/PunishmentHistory'
PunishmentHistory:
type: object type: object
required: required:
- username - username
@ -228,6 +260,7 @@ components:
- expiryTime - expiryTime
- punishedBy - punishedBy
- punishedByUuid - punishedByUuid
- id
properties: properties:
username: username:
type: string type: string
@ -262,6 +295,9 @@ components:
removedReason: removedReason:
type: string type: string
description: The reason why the punishment was removed description: The reason why the punishment was removed
id:
type: integer
description: Id of the punishment
Player: Player:
type: object type: object
properties: properties: