diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 8259339..3e37815 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -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 { diff --git a/api/src/main/java/com/alttd/chat/database/ChatLogQueries.java b/api/src/main/java/com/alttd/chat/database/ChatLogQueries.java index f1ab580..b056834 100644 --- a/api/src/main/java/com/alttd/chat/database/ChatLogQueries.java +++ b/api/src/main/java/com/alttd/chat/database/ChatLogQueries.java @@ -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 storeMessages(HashMap> 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> 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 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) { diff --git a/api/src/main/java/com/alttd/chat/database/DatabaseConnection.java b/api/src/main/java/com/alttd/chat/database/DatabaseConnection.java index ba4d63c..c4a3d25 100755 --- a/api/src/main/java/com/alttd/chat/database/DatabaseConnection.java +++ b/api/src/main/java/com/alttd/chat/database/DatabaseConnection.java @@ -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; } -} \ No newline at end of file +} diff --git a/api/src/main/java/com/alttd/chat/objects/Toggleable.java b/api/src/main/java/com/alttd/chat/objects/Toggleable.java index 1c53607..1aa7ffb 100644 --- a/api/src/main/java/com/alttd/chat/objects/Toggleable.java +++ b/api/src/main/java/com/alttd/chat/objects/Toggleable.java @@ -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; } diff --git a/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLog.java b/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLog.java index fbe5d1c..df260e7 100644 --- a/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLog.java +++ b/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLog.java @@ -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); } } diff --git a/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLogHandler.java b/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLogHandler.java index f551262..9764447 100644 --- a/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLogHandler.java +++ b/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLogHandler.java @@ -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 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); } diff --git a/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLogMapper.java b/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLogMapper.java new file mode 100644 index 0000000..3ab446c --- /dev/null +++ b/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLogMapper.java @@ -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 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> violations = validator.validate(chatMessageDto); + if (violations.isEmpty()) { + return false; + } + log.error("Validation failed for chat message: {}, {}", chatMessageDto, violations); + return true; + } +} diff --git a/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLogType.java b/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLogType.java new file mode 100644 index 0000000..dfd3771 --- /dev/null +++ b/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLogType.java @@ -0,0 +1,10 @@ +package com.alttd.chat.objects.chat_log; + +public enum ChatLogType { + PUBLIC, + GLOBAL, + PARTY, + GAC, + MSG, + CUSTOM; +} diff --git a/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLogTypeMapper.java b/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLogTypeMapper.java new file mode 100644 index 0000000..941fb23 --- /dev/null +++ b/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLogTypeMapper.java @@ -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); + }; + } + +} diff --git a/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLogWebHandler.java b/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLogWebHandler.java index 40fd25b..c8e7697 100644 --- a/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLogWebHandler.java +++ b/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLogWebHandler.java @@ -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 batch = chatLogList.stream().map(ChatLog::toDto).toList(); + List batch = chatLogList.stream() + .map(ChatLogMapper::toDto) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); if (!chatLogList.isEmpty()) { try { diff --git a/galaxy/src/main/java/com/alttd/chat/ChatPlugin.java b/galaxy/src/main/java/com/alttd/chat/ChatPlugin.java index a2b4c08..4ef5f6e 100755 --- a/galaxy/src/main/java/com/alttd/chat/ChatPlugin.java +++ b/galaxy/src/main/java/com/alttd/chat/ChatPlugin.java @@ -35,11 +35,15 @@ public class ChatPlugin extends JavaPlugin { instance = this; ALogger.init(getSLF4JLogger()); chatAPI = new ChatImplementation(); - chatHandler = new ChatHandler(); + ChatLogHandler chatLogHandler = ChatLogHandler.getInstance(true); + chatHandler = new ChatHandler(chatLogHandler); DatabaseConnection.initialize(); serverConfig = new ServerConfig(ServerName.getServerName()); - ChatLogHandler chatLogHandler = ChatLogHandler.getInstance(true); - registerListener(new PlayerListener(serverConfig), new ChatListener(chatLogHandler), new BookListener(), new ShutdownListener(chatLogHandler, this)); + registerListener(new PlayerListener(serverConfig), + new ChatListener(chatLogHandler), + new BookListener(), + new ShutdownListener(chatLogHandler, this) + ); if (serverConfig.GLOBALCHAT) { registerCommand("globalchat", new GlobalChat()); registerCommand("toggleglobalchat", new ToggleGlobalChat()); @@ -58,12 +62,15 @@ public class ChatPlugin extends JavaPlugin { if (!(channel instanceof CustomChannel customChannel)) { continue; } - this.getServer().getCommandMap().register(channel.getChannelName().toLowerCase(), new ChatChannel(customChannel)); + this.getServer() + .getCommandMap() + .register(channel.getChannelName().toLowerCase(), new ChatChannel(customChannel)); } String messageChannel = Config.MESSAGECHANNEL; getServer().getMessenger().registerOutgoingPluginChannel(this, messageChannel); - getServer().getMessenger().registerIncomingPluginChannel(this, messageChannel, new PluginMessage()); + getServer().getMessenger() + .registerIncomingPluginChannel(this, messageChannel, new PluginMessage(chatLogHandler)); NicknamesEvents nicknamesEvents = new NicknamesEvents(); getServer().getMessenger().registerIncomingPluginChannel(this, messageChannel, nicknamesEvents); @@ -110,7 +117,9 @@ public class ChatPlugin extends JavaPlugin { chatAPI.reloadConfig(); chatAPI.reloadChatFilters(); serverConfig = new ServerConfig(ServerName.getServerName()); - Bukkit.broadcast(Utility.parseMiniMessage("Reloaded ChatPlugin Config.").asComponent(), "command.chat.reloadchat"); + Bukkit.broadcast(Utility.parseMiniMessage("Reloaded ChatPlugin Config.").asComponent(), + "command.chat.reloadchat" + ); ALogger.info("Reloaded ChatPlugin config."); } } diff --git a/galaxy/src/main/java/com/alttd/chat/handler/ChatHandler.java b/galaxy/src/main/java/com/alttd/chat/handler/ChatHandler.java index 733952c..327d5a6 100755 --- a/galaxy/src/main/java/com/alttd/chat/handler/ChatHandler.java +++ b/galaxy/src/main/java/com/alttd/chat/handler/ChatHandler.java @@ -8,6 +8,8 @@ import com.alttd.chat.objects.ChatFilter; import com.alttd.chat.objects.ChatUser; import com.alttd.chat.objects.ModifiableString; import com.alttd.chat.objects.channels.CustomChannel; +import com.alttd.chat.objects.chat_log.ChatLogHandler; +import com.alttd.chat.objects.chat_log.ChatLogType; import com.alttd.chat.util.ALogger; import com.alttd.chat.util.GalaxyUtility; import com.alttd.chat.util.ServerName; @@ -28,6 +30,7 @@ import org.bukkit.Material; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; +import org.jspecify.annotations.Nullable; import java.util.List; import java.util.Objects; @@ -40,8 +43,10 @@ public class ChatHandler { private final ChatPlugin plugin; private final ComponentLike GCNOTENABLED; + private final ChatLogHandler chatLogHandler; - public ChatHandler() { + public ChatHandler(ChatLogHandler chatLogHandler) { + this.chatLogHandler = chatLogHandler; plugin = ChatPlugin.getInstance(); GCNOTENABLED = Utility.parseMiniMessage(Config.GCNOTENABLED); } @@ -54,7 +59,7 @@ public class ChatHandler { Placeholder.component("message", parseMessageContent(player, message)), Placeholder.component("sendername", player.name()), Placeholder.parsed("receivername", target) - ); + ); ComponentLike component = Utility.parseMiniMessage("", placeholders); @@ -62,9 +67,10 @@ public class ChatHandler { // todo a better way for this if (!RegexManager.filterText(player.getName(), player.getUniqueId(), modifiableString, "privatemessage")) { GalaxyUtility.sendBlockedNotification("DM Language", - player, - Utility.parseMiniMessage(Utility.parseColors(modifiableString.string())), - target); + player, + Utility.parseMiniMessage(Utility.parseColors(modifiableString.string())), + target + ); return; // the message was blocked } @@ -73,7 +79,8 @@ public class ChatHandler { sendPrivateMessage(player, target, "privatemessage", component.asComponent()); ComponentLike spymessage = Utility.parseMiniMessage(Config.MESSAGESPY, placeholders); for (Player pl : Bukkit.getOnlinePlayers()) { - if (pl.hasPermission(Config.SPYPERMISSION) && ChatUserManager.getChatUser(pl.getUniqueId()).isSpy() && !pl.equals(player) && !pl.getName().equalsIgnoreCase(target)) { + if (pl.hasPermission(Config.SPYPERMISSION) && ChatUserManager.getChatUser(pl.getUniqueId()) + .isSpy() && !pl.equals(player) && !pl.getName().equalsIgnoreCase(target)) { pl.sendMessage(spymessage); } } @@ -88,15 +95,16 @@ public class ChatHandler { Placeholder.component("message", messageComponent), Placeholder.component("sendername", player.name()), Placeholder.parsed("receivername", target) - ); + ); ModifiableString modifiableString = new ModifiableString(messageComponent); // todo a better way for this if (!RegexManager.filterText(player.getName(), player.getUniqueId(), modifiableString, "privatemessage")) { GalaxyUtility.sendBlockedNotification("DM Language", - player, - Utility.parseMiniMessage(Utility.parseColors(modifiableString.string())), - target); + player, + Utility.parseMiniMessage(Utility.parseColors(modifiableString.string())), + target + ); return; // the message was blocked } @@ -108,7 +116,8 @@ public class ChatHandler { sendPrivateMessage(player, target, "privatemessage", messageComponent); ComponentLike spymessage = Utility.parseMiniMessage(Config.MESSAGESPY, placeholders); for (Player pl : Bukkit.getOnlinePlayers()) { - if (pl.hasPermission(Config.SPYPERMISSION) && ChatUserManager.getChatUser(pl.getUniqueId()).isSpy() && !pl.equals(player) && !pl.getName().equalsIgnoreCase(target)) { + if (pl.hasPermission(Config.SPYPERMISSION) && ChatUserManager.getChatUser(pl.getUniqueId()) + .isSpy() && !pl.equals(player) && !pl.getName().equalsIgnoreCase(target)) { pl.sendMessage(spymessage); } } @@ -127,7 +136,9 @@ public class ChatHandler { long timeLeft = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis() - user.getGcCooldown()); if (timeLeft <= Config.GCCOOLDOWN && !player.hasPermission("chat.globalchat.cooldownbypass")) { // player is on cooldown and should wait x seconds - player.sendRichMessage(Config.GCONCOOLDOWN, Placeholder.parsed("cooldown", Config.GCCOOLDOWN - timeLeft + "")); + player.sendRichMessage(Config.GCONCOOLDOWN, + Placeholder.parsed("cooldown", Config.GCCOOLDOWN - timeLeft + "") + ); return; } @@ -138,7 +149,7 @@ public class ChatHandler { Placeholder.component("prefix", prefix), Placeholder.component("message", parseMessageContent(player, message)), Placeholder.parsed("server", ServerName.getServerName()) - ); + ); Component component = Utility.parseMiniMessage(Config.GCFORMAT, placeholders).asComponent(); @@ -146,13 +157,24 @@ public class ChatHandler { // todo a better way for this if (!RegexManager.filterText(player.getName(), player.getUniqueId(), modifiableString, "globalchat")) { GalaxyUtility.sendBlockedNotification("GC Language", - player, - Utility.parseMiniMessage(Utility.parseColors(modifiableString.string())), - ""); + player, + Utility.parseMiniMessage(Utility.parseColors(modifiableString.string())), + "" + ); return; // the message was blocked } component = modifiableString.component(); + chatLogHandler.addChatLog(player.getUniqueId(), + Bukkit.getServerName(), + message, + ChatLogType.GLOBAL, + null, + null, + component, + false + ); + user.setGcCooldown(System.currentTimeMillis()); sendPluginMessage(player, "globalchat", component); } @@ -176,15 +198,20 @@ public class ChatHandler { Placeholder.component("message", parseMessageContent(player, message)), Placeholder.parsed("server", ServerName.getServerName()), Placeholder.parsed("channel", channel.getChannelName()) - ); + ); Component component = Utility.parseMiniMessage(channel.getFormat(), placeholders).asComponent(); ModifiableString modifiableString = new ModifiableString(component); - if (!RegexManager.filterText(player.getName(), player.getUniqueId(), modifiableString, channel.getChannelName())) { + if (!RegexManager.filterText(player.getName(), + player.getUniqueId(), + modifiableString, + channel.getChannelName() + )) { GalaxyUtility.sendBlockedNotification(channel.getChannelName() + " Language", - player, - Utility.parseMiniMessage(Utility.parseColors(modifiableString.string())), - ""); + player, + Utility.parseMiniMessage(Utility.parseColors(modifiableString.string())), + "" + ); ALogger.info("Refusing to send blocked chat message"); return; } @@ -205,7 +232,7 @@ public class ChatHandler { out.writeUTF(message); out.writeUTF(GsonComponentSerializer.gson().serialize( itemComponent(player.getInventory().getItemInMainHand()) - )); + )); player.sendPluginMessage(plugin, Config.MESSAGECHANNEL, out.toByteArray()); // if (isMuted(player, message, "[" + party.getPartyName() + " Muted] ")) return; @@ -255,16 +282,20 @@ public class ChatHandler { if (!chatChannel.getServers().contains(ServerName.getServerName())) { player.sendRichMessage("Unable to send messages to in this server.", - Placeholder.parsed("channel", chatChannel.getChannelName())); + Placeholder.parsed("channel", chatChannel.getChannelName()) + ); ALogger.info(String.format("Not sending chat message due to [%s] not being in this channels config", - ServerName.getServerName())); + ServerName.getServerName() + )); return; } Stream stream = Bukkit.getServer().getOnlinePlayers().stream() .filter(p -> p.hasPermission(chatChannel.getPermission())); if (!player.hasPermission("chat.ignorebypass")) { - stream = stream.filter(receiver -> !ChatUserManager.getChatUser(receiver.getUniqueId()).getIgnoredPlayers().contains(uuid) + stream = stream.filter(receiver -> !ChatUserManager.getChatUser(receiver.getUniqueId()) + .getIgnoredPlayers() + .contains(uuid) || receiver.hasPermission("chat.ignorebypass")); } if (chatChannel.isLocal()) { @@ -283,14 +314,39 @@ public class ChatHandler { List recipientPlayers = stream.toList(); recipientPlayers.forEach(p -> p.sendMessage(component)); + chatLogHandler.addChatLog(player.getUniqueId(), + Bukkit.getServerName(), + message, + ChatLogType.CUSTOM, + chatChannel.getChannelName(), + getLocationIfLocal(chatChannel, player), + component.asComponent(), + false + ); + List recipientUUIDs = recipientPlayers.stream().map(Entity::getUniqueId).toList(); Bukkit.getServer().getOnlinePlayers().stream() .filter(onlinePlayer -> onlinePlayer.hasPermission(Config.SPYPERMISSION)) .filter(onlinePlayer -> !recipientUUIDs.contains(onlinePlayer.getUniqueId())) .forEach(onlinePlayer -> onlinePlayer.sendRichMessage(Config.CHANNEL_SPY, - Placeholder.component("sender", player.name()), - Placeholder.parsed("channel", chatChannel.getChannelName()), - Placeholder.parsed("message", message))); + Placeholder.component("sender", player.name()), + Placeholder.parsed("channel", chatChannel.getChannelName()), + Placeholder.parsed("message", message) + )); + } + + private static @Nullable String getLocationIfLocal(CustomChannel chatChannel, Player player) { + if (!chatChannel.isLocal()) { + return null; + } + String format = String.format("%s;%s", + player.getLocation().getBlockX(), + player.getLocation().getBlockZ() + ); + if (format.length() > 36) { + return null; + } + return format; } private void sendPluginMessage(Player player, String channel, Component component) { @@ -326,9 +382,14 @@ public class ChatHandler { if (user == null) { return false; } - if (user.isMuted() || (ChatPlugin.getInstance().serverMuted() && !player.hasPermission("chat.bypass-server-muted"))) { + if (user.isMuted() || (ChatPlugin.getInstance().serverMuted() && !player.hasPermission( + "chat.bypass-server-muted"))) { // if (Database.get().isPlayerMuted(player.getUniqueId(), null) || (ChatPlugin.getInstance().serverMuted() && !player.hasPermission("chat.bypass-server-muted"))) { - GalaxyUtility.sendBlockedNotification(prefix, player, Utility.parseMiniMessage(Utility.stripTokens(message)), ""); + GalaxyUtility.sendBlockedNotification(prefix, + player, + Utility.parseMiniMessage(Utility.stripTokens(message)), + "" + ); return true; } return false; diff --git a/galaxy/src/main/java/com/alttd/chat/listeners/ChatListener.java b/galaxy/src/main/java/com/alttd/chat/listeners/ChatListener.java index 957fc89..7eac7bc 100755 --- a/galaxy/src/main/java/com/alttd/chat/listeners/ChatListener.java +++ b/galaxy/src/main/java/com/alttd/chat/listeners/ChatListener.java @@ -7,6 +7,7 @@ import com.alttd.chat.managers.ChatUserManager; import com.alttd.chat.managers.RegexManager; import com.alttd.chat.objects.*; import com.alttd.chat.objects.chat_log.ChatLogHandler; +import com.alttd.chat.objects.chat_log.ChatLogType; import com.alttd.chat.util.ALogger; import com.alttd.chat.util.GalaxyUtility; import com.alttd.chat.util.ServerName; @@ -129,6 +130,9 @@ public class ChatListener implements Listener { chatLogHandler.addChatLog(uuid, ServerName.getServerName(), originalMessage, + ChatLogType.PUBLIC, + null, + null, render(player, inputComponent).asComponent(), true ); @@ -162,6 +166,9 @@ public class ChatListener implements Listener { chatLogHandler.addChatLog(uuid, ServerName.getServerName(), modifiableString.string(), + ChatLogType.PUBLIC, + null, + null, input.asComponent(), false ); diff --git a/galaxy/src/main/java/com/alttd/chat/listeners/PluginMessage.java b/galaxy/src/main/java/com/alttd/chat/listeners/PluginMessage.java index 90ae881..1bb5713 100755 --- a/galaxy/src/main/java/com/alttd/chat/listeners/PluginMessage.java +++ b/galaxy/src/main/java/com/alttd/chat/listeners/PluginMessage.java @@ -10,6 +10,8 @@ import com.alttd.chat.objects.Party; import com.alttd.chat.objects.PartyUser; import com.alttd.chat.objects.channels.Channel; import com.alttd.chat.objects.channels.CustomChannel; +import com.alttd.chat.objects.chat_log.ChatLogHandler; +import com.alttd.chat.objects.chat_log.ChatLogType; import com.alttd.chat.util.ALogger; import com.alttd.chat.util.ServerName; import com.alttd.chat.util.Utility; @@ -18,6 +20,7 @@ import com.google.common.io.ByteStreams; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.ComponentLike; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.bukkit.Bukkit; import org.bukkit.Sound; import org.bukkit.entity.Player; @@ -29,6 +32,12 @@ import java.util.UUID; public class PluginMessage implements PluginMessageListener { + private final ChatLogHandler chatLogHandler; + + public PluginMessage(ChatLogHandler chatLogHandler) { + this.chatLogHandler = chatLogHandler; + } + @Override public void onPluginMessageReceived(String channel, @NotNull Player ignored, byte[] bytes) { if (!channel.equals(Config.MESSAGECHANNEL)) { @@ -48,13 +57,25 @@ public class PluginMessage implements PluginMessageListener { } ChatUser chatUser = ChatUserManager.getChatUser(uuid); if (isTargetNotIgnored(chatUser, targetuuid)) { - player.sendMessage(GsonComponentSerializer.gson().deserialize(message)); + Component component = GsonComponentSerializer.gson().deserialize(message); + player.sendMessage(component); player.playSound(player.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1, - 1); // todo load this from config + 1 + ); // todo load this from config ChatUser user = ChatUserManager.getChatUser(uuid); if (!user.getReplyContinueTarget().equalsIgnoreCase(target)) { user.setReplyTarget(target); } + // Handled here since this is after the ignore check and is only send once + chatLogHandler.addChatLog(uuid, + ServerName.getServerName(), + PlainTextComponentSerializer.plainText().serialize(component), + ChatLogType.MSG, + null, + player.getUniqueId().toString(), + component, + false + ); } } case "privatemessageout" -> { @@ -69,9 +90,18 @@ public class PluginMessage implements PluginMessageListener { ChatUser chatUser = ChatUserManager.getChatUser(uuid); if (isTargetNotIgnored(chatUser, targetuuid)) { chatUser.setReplyTarget(target); - player.sendMessage(GsonComponentSerializer.gson().deserialize(message)); - // ChatUser user = ChatUserManager.getChatUser(uuid); - // user.setReplyTarget(target); + Component component = GsonComponentSerializer.gson().deserialize(message); + player.sendMessage(component); + // Handled here since this is after the ignore check and is only send once + chatLogHandler.addChatLog(uuid, + ServerName.getServerName(), + PlainTextComponentSerializer.plainText().serialize(component), + ChatLogType.MSG, + null, + player.getUniqueId().toString(), + component, + false + ); } } case "globalchat" -> { @@ -82,10 +112,11 @@ public class PluginMessage implements PluginMessageListener { UUID uuid = UUID.fromString(in.readUTF()); String message = in.readUTF(); + Component component = GsonComponentSerializer.gson().deserialize(message); Bukkit.getOnlinePlayers().stream().filter(p -> p.hasPermission(Config.GCPERMISSION)).forEach(p -> { ChatUser chatUser = ChatUserManager.getChatUser(p.getUniqueId()); if (isTargetNotIgnored(chatUser, uuid)) { - p.sendMessage(GsonComponentSerializer.gson().deserialize(message)); + p.sendMessage(component); } }); } @@ -107,6 +138,7 @@ public class PluginMessage implements PluginMessageListener { } chatChannel(in); + //TODO [Stijn] [2026-07-19]: handle custom channels } case "tmppartyupdate" -> { int id = Integer.parseInt(in.readUTF()); diff --git a/velocity/src/main/java/com/alttd/velocitychat/VelocityChat.java b/velocity/src/main/java/com/alttd/velocitychat/VelocityChat.java index 9833870..c0a4bbd 100755 --- a/velocity/src/main/java/com/alttd/velocitychat/VelocityChat.java +++ b/velocity/src/main/java/com/alttd/velocitychat/VelocityChat.java @@ -2,20 +2,20 @@ package com.alttd.velocitychat; import com.alttd.chat.ChatAPI; import com.alttd.chat.ChatImplementation; +import com.alttd.chat.config.Config; +import com.alttd.chat.database.DatabaseConnection; import com.alttd.chat.managers.ChatUserManager; import com.alttd.chat.managers.PartyManager; import com.alttd.chat.objects.ChatUser; import com.alttd.chat.objects.chat_log.ChatLogHandler; +import com.alttd.chat.util.ALogger; import com.alttd.velocitychat.commands.*; -import com.alttd.chat.config.Config; -import com.alttd.chat.database.DatabaseConnection; import com.alttd.velocitychat.handlers.ChatHandler; import com.alttd.velocitychat.handlers.ServerHandler; import com.alttd.velocitychat.listeners.ChatListener; import com.alttd.velocitychat.listeners.LiteBansListener; -import com.alttd.velocitychat.listeners.ProxyPlayerListener; import com.alttd.velocitychat.listeners.PluginMessageListener; -import com.alttd.chat.util.ALogger; +import com.alttd.velocitychat.listeners.ProxyPlayerListener; import com.google.common.io.ByteArrayDataOutput; import com.google.common.io.ByteStreams; import com.google.inject.Inject; @@ -36,7 +36,7 @@ import java.nio.file.Path; description = "A chat plugin for Altitude Minecraft Server", authors = {"destro174", "teri"}, dependencies = {@Dependency(id = "luckperms"), @Dependency(id = "litebans"), @Dependency(id = "proxydiscordlink")} - ) +) public class VelocityChat { private static VelocityChat plugin; @@ -67,7 +67,8 @@ public class VelocityChat { PartyManager.initialize(); // load the parties from the db and add the previously loaded users to them serverHandler = new ServerHandler(); - chatHandler = new ChatHandler(); + ChatLogHandler chatLogHandler = new ChatLogHandler(true); + chatHandler = new ChatHandler(chatLogHandler); server.getEventManager().register(this, new ChatListener()); server.getEventManager().register(this, new ProxyPlayerListener()); new LiteBansListener().init(); // init the litebans api listeners @@ -89,7 +90,11 @@ public class VelocityChat { ByteArrayDataOutput buf = ByteStreams.newDataOutput(); buf.writeUTF("reloadconfig"); ALogger.info("Reloaded ChatPlugin proxy config."); - getProxy().getAllServers().stream().forEach(registeredServer -> registeredServer.sendPluginMessage(getChannelIdentifier(), buf.toByteArray())); + getProxy().getAllServers() + .stream() + .forEach(registeredServer -> registeredServer.sendPluginMessage(getChannelIdentifier(), + buf.toByteArray() + )); } public File getDataDirectory() { diff --git a/velocity/src/main/java/com/alttd/velocitychat/handlers/ChatHandler.java b/velocity/src/main/java/com/alttd/velocitychat/handlers/ChatHandler.java index ed131d9..0e64c54 100755 --- a/velocity/src/main/java/com/alttd/velocitychat/handlers/ChatHandler.java +++ b/velocity/src/main/java/com/alttd/velocitychat/handlers/ChatHandler.java @@ -6,6 +6,8 @@ import com.alttd.chat.managers.ChatUserManager; import com.alttd.chat.managers.PartyManager; import com.alttd.chat.managers.RegexManager; import com.alttd.chat.objects.*; +import com.alttd.chat.objects.chat_log.ChatLogHandler; +import com.alttd.chat.objects.chat_log.ChatLogType; import com.alttd.chat.util.ALogger; import com.alttd.chat.util.Utility; import com.alttd.velocitychat.VelocityChat; @@ -32,6 +34,12 @@ import java.util.UUID; public class ChatHandler { + private final ChatLogHandler chatLogHandler; + + public ChatHandler(ChatLogHandler chatLogHandler) { + this.chatLogHandler = chatLogHandler; + } + public void privateMessage(String sender, String target, String message) { UUID uuid = UUID.fromString(sender); ChatUser senderUser = ChatUserManager.getChatUser(uuid); @@ -54,15 +62,23 @@ public class ChatHandler { Placeholder.component("receiver", targetUser.getDisplayName()), Placeholder.unparsed("receivername", player2.getUsername()), Placeholder.component("message", GsonComponentSerializer.gson().deserialize(message)), - Placeholder.unparsed("server", player.getCurrentServer().isPresent() ? player.getCurrentServer().get().getServerInfo().getName() : "Altitude")); + Placeholder.unparsed("server", + player.getCurrentServer().isPresent() ? player.getCurrentServer() + .get() + .getServerInfo() + .getName() : "Altitude" + ) + ); ServerConnection serverConnection; + //Log handled on receiving server since it is only received on one server and that's where the ignore check happens if (player.getCurrentServer().isPresent() && player2.getCurrentServer().isPresent()) { // redirect to the sender serverConnection = player.getCurrentServer().get(); Component component = Utility.parseMiniMessage(Config.MESSAGESENDER - .replaceAll("", player.getUsername()) - .replaceAll("", player2.getUsername()), Placeholders).asComponent(); + .replaceAll("", player.getUsername()) + .replaceAll("", player2.getUsername()), Placeholders + ).asComponent(); ByteArrayDataOutput buf = ByteStreams.newDataOutput(); buf.writeUTF("privatemessageout"); buf.writeUTF(player.getUniqueId().toString()); @@ -74,8 +90,9 @@ public class ChatHandler { //redirect to the receiver serverConnection = player2.getCurrentServer().get(); component = Utility.parseMiniMessage(Config.MESSAGERECIEVER - .replaceAll("", player.getUsername()) - .replaceAll("", player2.getUsername()), Placeholders).asComponent(); + .replaceAll("", player.getUsername()) + .replaceAll("", player2.getUsername()), Placeholders + ).asComponent(); buf = ByteStreams.newDataOutput(); buf.writeUTF("privatemessagein"); buf.writeUTF(player2.getUniqueId().toString()); @@ -93,7 +110,7 @@ public class ChatHandler { Placeholder.parsed("displayname", Utility.getDisplayName(player.getUniqueId(), player.getUsername())), Placeholder.unparsed("target", (target.isEmpty() ? " tried to say: " : " -> " + target + ": ")), Placeholder.unparsed("input", input) - ); + ); ComponentLike blockedNotification = Utility.parseMiniMessage(Config.NOTIFICATIONFORMAT, Placeholders); serverConnection.getServer().getPlayersConnected().forEach(pl -> { @@ -134,15 +151,15 @@ public class ChatHandler { } ComponentLike senderName = user.getDisplayName(); - TagResolver Placeholders = TagResolver.resolver( + TagResolver placeholders = TagResolver.resolver( Placeholder.component("sender", senderName), Placeholder.component("sendername", senderName), Placeholder.unparsed("partyname", party.getPartyName()), Placeholder.component("message", parseMessageContent(player, message)), Placeholder.unparsed("server", serverConnection.getServer().getServerInfo().getName()) - ); + ); - Component partyMessage = Utility.parseMiniMessage(Config.PARTY_FORMAT, Placeholders).asComponent() + Component partyMessage = Utility.parseMiniMessage(Config.PARTY_FORMAT, placeholders).asComponent() .replaceText(TextReplacementConfig.builder().once().matchLiteral("[i]").replacement(item).build()); ModifiableString modifiableString = new ModifiableString(partyMessage); @@ -155,7 +172,17 @@ public class ChatHandler { sendPartyMessage(party, partyMessage, user.getIgnoredBy()); - ComponentLike spyMessage = Utility.parseMiniMessage(Config.PARTY_SPY, Placeholders); + chatLogHandler.addChatLog(uuid, + serverConnection.getServer().getServerInfo().getName(), + PlainTextComponentSerializer.plainText().serialize(partyMessage), + ChatLogType.PARTY, + String.valueOf(party.getPartyId()), + null, + partyMessage, + false + ); + + ComponentLike spyMessage = Utility.parseMiniMessage(Config.PARTY_SPY, placeholders); for (Player pl : serverConnection.getServer().getPlayersConnected()) { if (pl.hasPermission(Config.SPYPERMISSION) && !party.getPartyUsersUuid().contains(pl.getUniqueId())) { pl.sendMessage(spyMessage); @@ -171,6 +198,17 @@ public class ChatHandler { .stream() .filter(target -> target.hasPermission("command.chat.globaladminchat")) .forEach(target -> target.sendMessage(component)); + + chatLogHandler.addChatLog( + new UUID(0, 0), //No origin uuid for GAC + "GAC", //No server for GAC + PlainTextComponentSerializer.plainText().serialize(component), + ChatLogType.GAC, + null, + null, + component, + false + ); } public void globalAdminChat(CommandSource commandSource, String message) { @@ -182,13 +220,17 @@ public class ChatHandler { return; } senderName = user.getDisplayName(); - serverName = sender.getCurrentServer().isPresent() ? sender.getCurrentServer().get().getServerInfo().getName() : "Altitude"; + serverName = sender.getCurrentServer().isPresent() ? sender.getCurrentServer() + .get() + .getServerInfo() + .getName() : "Altitude"; } TagResolver Placeholders = TagResolver.resolver( Placeholder.component("message", parseMessageContent(commandSource, message)), Placeholder.component("sender", senderName), - Placeholder.unparsed("server", serverName)); + Placeholder.unparsed("server", serverName) + ); ComponentLike component = Utility.parseMiniMessage(Config.GACFORMAT, Placeholders); @@ -210,7 +252,8 @@ public class ChatHandler { if (optionalPlayer.isEmpty()) { targetUUID = ServerHandler.getPlayerUUID(recipient); if (targetUUID == null) { - commandSource.sendMessage(Utility.parseMiniMessage("A player with this name hasn't logged in recently.")); // TOOD load from config + commandSource.sendMessage(Utility.parseMiniMessage( + "A player with this name hasn't logged in recently.")); // TOOD load from config return; } } else { @@ -274,8 +317,10 @@ public class ChatHandler { Placeholder.component("sender", chatUser.getDisplayName()), Placeholder.component("message", Utility.parseMiniMessage(mail.getMessage())), Placeholder.unparsed("date", date.toString()), - Placeholder.unparsed("time_ago", getTimeAgo(Duration.between(date.toInstant(), new Date().toInstant()))) - ); + Placeholder.unparsed("time_ago", + getTimeAgo(Duration.between(date.toInstant(), new Date().toInstant())) + ) + ); ComponentLike mailMessage = Utility.parseMiniMessage(Config.mailBody, Placeholders); component = component.append(Component.newline()).append(mailMessage); } diff --git a/web-api/src/main/resources/chat-api.yml b/web-api/src/main/resources/chat-api.yml index 35b273d..db9610e 100644 --- a/web-api/src/main/resources/chat-api.yml +++ b/web-api/src/main/resources/chat-api.yml @@ -54,6 +54,7 @@ components: - uuid - name - styledName + # TODO [Stijn] [2026-07-19]: Add who they ignore and who they are ignored by properties: uuid: @@ -69,25 +70,33 @@ components: required: - uuid - message - - channel - server + - type - timestamp - blocked properties: uuid: type: string format: uuid - channel: - type: string - enum: - - CHAT - - PARTY - - AC - - GAC message: type: string server: type: string + type: + type: string + enum: + - PUBLIC + - GLOBAL + - PARTY + - GAC + - MSG + - CUSTOM + channel: + type: string + maxLength: 36 + receiver: + type: string + maxLength: 36 timestamp: type: string format: date-time