Integrate ChatLogHandler across chat modules and add centralized message logging with validation through ChatLogMapper.

This commit is contained in:
2026-07-19 23:00:40 +02:00
parent 92979e9789
commit 7492dfa418
17 changed files with 418 additions and 115 deletions
+4
View File
@@ -12,6 +12,10 @@ dependencies {
}
compileOnly("org.spongepowered:configurate-yaml:4.2.0") // Configurate
compileOnly("net.luckperms:api:5.5") // Luckperms
//API validation
implementation("org.hibernate.validator:hibernate-validator:9.0.1.Final")
implementation("org.glassfish:jakarta.el:5.0.0-M1")
}
publishing {
@@ -16,11 +16,14 @@ public class ChatLogQueries {
protected static void createChatLogTable() {
String nicknamesTableQuery = """
CREATE TABLE chat_log (
CREATE TABLE IF NOT EXISTSq:Q chat_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
uuid CHAR(36) NOT NULL,
time_stamp TIMESTAMP(6) NOT NULL,
server VARCHAR(50) NOT NULL,
type VARCHAR(16) NOT NULL DEFAULT 'public',
channel VARCHAR(36) DEFAULT NULL,
receiver CHAR(36) DEFAULT NULL,
chat_message VARCHAR(300) NOT NULL,
mini_message JSON,
blocked BOOLEAN NOT NULL DEFAULT FALSE,
@@ -38,7 +41,7 @@ public class ChatLogQueries {
}
public static @NotNull CompletableFuture<Boolean> storeMessages(HashMap<UUID, List<ChatLog>> chatMessages) {
String insertQuery = "INSERT INTO chat_log (uuid, time_stamp, server, chat_message, mini_message, blocked) VALUES (?, ?, ?, ?, ?, ?)";
String insertQuery = "INSERT INTO chat_log (uuid, time_stamp, server, type, channel, receiver, chat_message, mini_message, blocked) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
return CompletableFuture.supplyAsync(() -> {
try (Connection connection = DatabaseConnection.createTransactionConnection()) {
PreparedStatement preparedStatement = connection.prepareStatement(insertQuery);
@@ -67,7 +70,7 @@ public class ChatLogQueries {
}
public static @NotNull CompletableFuture<List<ChatLog>> retrieveMessages(ChatLogHandler chatLogHandler, UUID uuid, Duration duration, String server) {
String query = "SELECT * FROM chat_log WHERE uuid = ? AND time_stamp > ? AND server = ?";
String query = "SELECT * FROM chat_log WHERE uuid = ? AND time_stamp > ? AND server = ? AND type = 'public'";
return CompletableFuture.supplyAsync(() -> {
try (Connection connection = DatabaseConnection.getConnection()) {
PreparedStatement preparedStatement = connection.prepareStatement(query);
@@ -77,8 +80,7 @@ public class ChatLogQueries {
ResultSet resultSet = preparedStatement.executeQuery();
List<ChatLog> chatLogs = new ArrayList<>();
while (resultSet.next()) {
ChatLog chatLog = chatLogHandler.loadFromResultSet(resultSet);
chatLogs.add(chatLog);
chatLogHandler.loadFromResultSet(resultSet).ifPresent(chatLogs::add);
}
return chatLogs;
} catch (SQLException sqlException) {
@@ -1,6 +1,5 @@
package com.alttd.chat.database;
import com.alttd.chat.config.Config;
import java.sql.Connection;
@@ -27,6 +26,7 @@ public class DatabaseConnection {
/**
* Opens the connection if it's not already open.
*
* @throws SQLException If it can't create the connection.
*/
public void openConnection() throws SQLException {
@@ -45,14 +45,16 @@ public class DatabaseConnection {
}
connection = DriverManager.getConnection(
"jdbc:mysql://" + Config.IP + ":" + Config.PORT + "/" + Config.DATABASE + "?autoReconnect=true"+
"&useSSL=false",
Config.USERNAME, Config.PASSWORD);
"jdbc:mysql://" + Config.IP + ":" + Config.PORT + "/" + Config.DATABASE + "?autoReconnect=true" +
"&useSSL=false&preserveInstants=true",
Config.USERNAME, Config.PASSWORD
);
}
}
/**
* Returns the connection for the database
*
* @return Returns the connection.
*/
public static Connection getConnection() {
@@ -69,13 +71,15 @@ public class DatabaseConnection {
* Creates a transactional database connection.
*
* @return A {@code Connection} object representing the transactional database connection.
*
* @throws SQLException If there is an error creating the database connection.
*/
public static Connection createTransactionConnection() throws SQLException {
connection = DriverManager.getConnection(
"jdbc:mysql://" + Config.IP + ":" + Config.PORT + "/" + Config.DATABASE + "?autoReconnect=true"+
"&useSSL=false",
Config.USERNAME, Config.PASSWORD);
"jdbc:mysql://" + Config.IP + ":" + Config.PORT + "/" + Config.DATABASE + "?autoReconnect=true" +
"&useSSL=false&preserveInstants=true",
Config.USERNAME, Config.PASSWORD
);
connection.setAutoCommit(false);
return connection;
}
@@ -88,4 +92,4 @@ public class DatabaseConnection {
return connection != null;
}
}
}
@@ -18,8 +18,9 @@ public abstract class Toggleable {
public static Toggleable getToggleable(UUID uuid) {
for (Toggleable toggleableClass : togglableClasses) {
if (toggleableClass.isToggled(uuid))
if (toggleableClass.isToggled(uuid)) {
return toggleableClass;
}
}
return null;
}
@@ -1,6 +1,5 @@
package com.alttd.chat.objects.chat_log;
import com.alttd.altitudeweb.model.ChatMessageDto;
import com.alttd.chat.objects.BatchInsertable;
import lombok.AllArgsConstructor;
import lombok.Getter;
@@ -12,22 +11,20 @@ import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.UUID;
@AllArgsConstructor
@Getter
public class ChatLog implements BatchInsertable {
@Getter
private final UUID uuid;
@Getter
private final Instant timestamp;
private final String server;
@Getter
private final ChatLogType type;
private final String channel;
private final String receiver;
private final String message;
@Getter
private final Component miniMessage;
@Getter
private final boolean blocked;
@Override
@@ -35,24 +32,13 @@ public class ChatLog implements BatchInsertable {
preparedStatement.setString(1, uuid.toString());
preparedStatement.setTimestamp(2, Timestamp.from(timestamp));
preparedStatement.setString(3, server);
preparedStatement.setString(4, message);
preparedStatement.setString(5,
preparedStatement.setString(4, ChatLogTypeMapper.toDb(type));
preparedStatement.setString(5, channel);
preparedStatement.setString(6, receiver);
preparedStatement.setString(7, message);
preparedStatement.setString(8,
miniMessage == null ? null : GsonComponentSerializer.gson().serialize(miniMessage)
);
preparedStatement.setInt(6, blocked ? 1 : 0);
}
public ChatMessageDto toDto() {
ChatMessageDto chatMessageDto = new ChatMessageDto();
if (miniMessage == null) {
throw new IllegalArgumentException("MiniMessage cannot be null");
}
chatMessageDto.setMessage(GsonComponentSerializer.gson().serialize(miniMessage));
chatMessageDto.setChannel(ChatMessageDto.ChannelEnum.CHAT);
chatMessageDto.setServer(server);
chatMessageDto.setTimestamp(timestamp.atOffset(ZoneOffset.UTC));
chatMessageDto.setUuid(uuid);
chatMessageDto.setBlocked(blocked);
return chatMessageDto;
preparedStatement.setInt(9, blocked ? 1 : 0);
}
}
@@ -3,6 +3,7 @@ package com.alttd.chat.objects.chat_log;
import com.alttd.chat.config.Config;
import com.alttd.chat.database.ChatLogQueries;
import com.alttd.chat.util.ALogger;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import org.jetbrains.annotations.NotNull;
@@ -14,6 +15,7 @@ import java.time.Instant;
import java.util.*;
import java.util.concurrent.*;
@Slf4j
public class ChatLogHandler {
private final ChatLogWebHandler chatLogWebHandler = new ChatLogWebHandler();
@@ -111,20 +113,48 @@ public class ChatLogHandler {
});
}
public ChatLog loadFromResultSet(@NotNull ResultSet resultSet) throws SQLException {
public Optional<ChatLog> loadFromResultSet(@NotNull ResultSet resultSet) throws SQLException {
UUID chatLogUUID = UUID.fromString(resultSet.getString("uuid"));
Instant chatTimestamp = resultSet.getTimestamp("time_stamp").toInstant();
String server = resultSet.getString("server");
String stringType = resultSet.getString("type");
ChatLogType type;
try {
type = ChatLogTypeMapper.fromDb(stringType);
} catch (Exception e) {
log.error("Failed to load chat log from result set: {}", e.getMessage());
return Optional.empty();
}
String channel = resultSet.getString("channel");
String receiver = resultSet.getString("receiver");
String chatMessage = resultSet.getString("chat_message");
String stringMiniMessage = resultSet.getString("mini_message");
Component miniMessage = stringMiniMessage == null ? null : GsonComponentSerializer.gson().deserialize(
stringMiniMessage);
boolean chatMessageBlocked = resultSet.getInt("blocked") == 1;
return new ChatLog(chatLogUUID, chatTimestamp, server, chatMessage, miniMessage, chatMessageBlocked);
return Optional.of(new ChatLog(chatLogUUID,
chatTimestamp,
server,
type,
channel,
receiver,
chatMessage,
miniMessage,
chatMessageBlocked
));
}
public void addChatLog(UUID uuid, String server, String message, Component miniMessage, boolean blocked) {
ChatLog chatLog = new ChatLog(uuid, Instant.now(), server, message, miniMessage, blocked);
public void addChatLog(UUID uuid, String server, String message, ChatLogType chatLogType, String channel, String receiver, Component miniMessage, boolean blocked) {
ChatLog chatLog = new ChatLog(uuid,
Instant.now(),
server,
chatLogType,
channel,
receiver,
message,
miniMessage,
blocked
);
addLog(chatLog);
chatLogWebHandler.forwardChatLogToWeb(chatLog);
}
@@ -0,0 +1,50 @@
package com.alttd.chat.objects.chat_log;
import com.alttd.altitudeweb.model.ChatMessageDto;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import java.time.ZoneOffset;
import java.util.Optional;
import java.util.Set;
@Slf4j
@UtilityClass
public class ChatLogMapper {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
public Optional<ChatMessageDto> toDto(ChatLog chatLog) {
ChatMessageDto chatMessageDto = new ChatMessageDto();
if (chatLog.getMiniMessage() == null) {
throw new IllegalArgumentException("MiniMessage cannot be null");
}
chatMessageDto.setUuid(chatLog.getUuid());
chatMessageDto.setTimestamp(chatLog.getTimestamp().atOffset(ZoneOffset.UTC));
chatMessageDto.setServer(chatLog.getServer());
chatMessageDto.setType(ChatLogTypeMapper.toDto(chatLog.getType()));
chatMessageDto.setChannel(chatLog.getChannel());
chatMessageDto.setReceiver(chatLog.getReceiver());
chatMessageDto.setMessage(GsonComponentSerializer.gson().serialize(chatLog.getMiniMessage()));
chatMessageDto.setBlocked(chatLog.isBlocked());
if (checkIsInvalid(chatMessageDto)) {
return Optional.empty();
}
return Optional.of(chatMessageDto);
}
private boolean checkIsInvalid(ChatMessageDto chatMessageDto) {
Set<ConstraintViolation<ChatMessageDto>> violations = validator.validate(chatMessageDto);
if (violations.isEmpty()) {
return false;
}
log.error("Validation failed for chat message: {}, {}", chatMessageDto, violations);
return true;
}
}
@@ -0,0 +1,10 @@
package com.alttd.chat.objects.chat_log;
public enum ChatLogType {
PUBLIC,
GLOBAL,
PARTY,
GAC,
MSG,
CUSTOM;
}
@@ -0,0 +1,43 @@
package com.alttd.chat.objects.chat_log;
import com.alttd.altitudeweb.model.ChatMessageDto;
import lombok.experimental.UtilityClass;
@UtilityClass
public class ChatLogTypeMapper {
public static String toDb(ChatLogType chatLogType) {
return switch (chatLogType) {
case PUBLIC -> "public";
case GLOBAL -> "global";
case PARTY -> "party";
case GAC -> "gac";
case MSG -> "msg";
case CUSTOM -> "custom";
};
}
public ChatMessageDto.TypeEnum toDto(ChatLogType chatLogType) {
return switch (chatLogType) {
case PUBLIC -> ChatMessageDto.TypeEnum.PUBLIC;
case GLOBAL -> ChatMessageDto.TypeEnum.GLOBAL;
case PARTY -> ChatMessageDto.TypeEnum.PARTY;
case GAC -> ChatMessageDto.TypeEnum.GAC;
case MSG -> ChatMessageDto.TypeEnum.MSG;
case CUSTOM -> ChatMessageDto.TypeEnum.CUSTOM;
};
}
public ChatLogType fromDb(String dbType) {
return switch (dbType) {
case "public" -> ChatLogType.PUBLIC;
case "global" -> ChatLogType.GLOBAL;
case "party" -> ChatLogType.PARTY;
case "gac" -> ChatLogType.GAC;
case "msg" -> ChatLogType.MSG;
case "custom" -> ChatLogType.CUSTOM;
default -> throw new IllegalArgumentException("Invalid chat log type: " + dbType);
};
}
}
@@ -11,6 +11,7 @@ import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
@@ -74,7 +75,11 @@ public class ChatLogWebHandler {
chatLogList.add(log);
}
List<ChatMessageDto> batch = chatLogList.stream().map(ChatLog::toDto).toList();
List<ChatMessageDto> batch = chatLogList.stream()
.map(ChatLogMapper::toDto)
.filter(Optional::isPresent)
.map(Optional::get)
.toList();
if (!chatLogList.isEmpty()) {
try {