diff --git a/api/src/main/java/com/alttd/chat/config/Config.java b/api/src/main/java/com/alttd/chat/config/Config.java index 2bc277c..f1e4c59 100755 --- a/api/src/main/java/com/alttd/chat/config/Config.java +++ b/api/src/main/java/com/alttd/chat/config/Config.java @@ -551,4 +551,12 @@ public final class Config { DEATH_MESSAGES_MAX_PER_PERIOD = getInt("death-messages.max-per-period", DEATH_MESSAGES_MAX_PER_PERIOD); DEATH_MESSAGES_LIMIT_PERIOD_MINUTES = getInt("death-messages.limit-period-minutes", DEATH_MESSAGES_LIMIT_PERIOD_MINUTES); } + + public static long CHAT_LOG_DELETE_OLDER_THAN_DAYS = 31; + public static long CHAT_LOG_SAVE_DELAY_MINUTES = 5; + + private static void chatLogSettings() { + CHAT_LOG_DELETE_OLDER_THAN_DAYS = getLong("chat-log.delete-older-than-days", CHAT_LOG_DELETE_OLDER_THAN_DAYS); + CHAT_LOG_SAVE_DELAY_MINUTES = getLong("chat-log.save-delay-minutes", CHAT_LOG_SAVE_DELAY_MINUTES); + } } diff --git a/api/src/main/java/com/alttd/chat/database/ChatLogQueries.java b/api/src/main/java/com/alttd/chat/database/ChatLogQueries.java new file mode 100644 index 0000000..ae3984d --- /dev/null +++ b/api/src/main/java/com/alttd/chat/database/ChatLogQueries.java @@ -0,0 +1,102 @@ +package com.alttd.chat.database; + +import com.alttd.chat.objects.chat_log.ChatLog; +import com.alttd.chat.objects.chat_log.ChatLogHandler; +import com.alttd.chat.util.ALogger; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import org.jetbrains.annotations.NotNull; + +import java.sql.*; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +public class ChatLogQueries { + + protected static void createChatLogTable() { + String nicknamesTableQuery = "CREATE TABLE IF NOT EXISTS chat_log(" + + "uuid CHAR(48) NOT NULL," + + "time_stamp TIMESTAMP(6) NOT NULL, " + + "server VARCHAR(50) NOT NULL, " + + "chat_message VARCHAR(300) NOT NULL, " + + "blocked BIT(1) NOT NULL DEFAULT 0" + + ")"; + + try (PreparedStatement preparedStatement = DatabaseConnection.getConnection().prepareStatement(nicknamesTableQuery)) { + preparedStatement.executeUpdate(); + } catch (Throwable throwable) { + ALogger.error("Failed to create chat log table", throwable); + } + } + + public static @NotNull CompletableFuture storeMessages(Object2ObjectOpenHashMap> chatMessages) { + String insertQuery = "INSERT INTO chat_log (uuid, time_stamp, server, chat_message, blocked) VALUES (?, ?, ?, ?, ?)"; + return CompletableFuture.supplyAsync(() -> { + try (Connection connection = DatabaseConnection.createTransactionConnection()) { + PreparedStatement preparedStatement = connection.prepareStatement(insertQuery); + for (List chatLogList : chatMessages.values()) { + for (ChatLog chatLog : chatLogList) { + chatLog.prepareStatement(preparedStatement); + preparedStatement.addBatch(); + } + } + int[] updatedRowsCount = preparedStatement.executeBatch(); + boolean isSuccess = Arrays.stream(updatedRowsCount).allMatch(i -> i >= 0); + + if (isSuccess) { + connection.commit(); + return true; + } else { + connection.rollback(); + ALogger.warn("Failed to store messages"); + return false; + } + } catch (SQLException sqlException) { + ALogger.error("Failed to store chat messages", sqlException); + throw new CompletionException("Failed to store chat messages", sqlException); + } + }); + } + + 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 = ?"; + return CompletableFuture.supplyAsync(() -> { + try (Connection connection = DatabaseConnection.getConnection()) { + PreparedStatement preparedStatement = connection.prepareStatement(query); + preparedStatement.setString(1, uuid.toString()); + preparedStatement.setTimestamp(2, Timestamp.from(Instant.now().minus(duration))); + preparedStatement.setString(3, server); + ResultSet resultSet = preparedStatement.executeQuery(); + List chatLogs = new ArrayList<>(); + while (resultSet.next()) { + ChatLog chatLog = chatLogHandler.loadFromResultSet(resultSet); + chatLogs.add(chatLog); + } + return chatLogs; + } catch (SQLException sqlException) { + ALogger.error(String.format("Failed to retrieve messages for user %s", uuid), sqlException); + throw new CompletionException(String.format("Failed to retrieve messages for user %s", uuid), sqlException); + } + }); + } + + public static CompletableFuture deleteOldMessages(Duration duration) { + String query = "DELETE FROM chat_log WHERE time_stamp > ?"; + + return CompletableFuture.supplyAsync(() -> { + try (Connection connection = DatabaseConnection.getConnection()) { + PreparedStatement preparedStatement = connection.prepareStatement(query); + preparedStatement.setTimestamp(1, Timestamp.from(Instant.now().minus(duration))); + return preparedStatement.execute(); + } catch (SQLException sqlException) { + ALogger.error(String.format("Failed to delete messages older than %s days", duration.toDays()), sqlException); + throw new CompletionException(String.format("Failed to delete messages older than %s days", duration.toDays()), 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 40e8988..ba4d63c 100755 --- a/api/src/main/java/com/alttd/chat/database/DatabaseConnection.java +++ b/api/src/main/java/com/alttd/chat/database/DatabaseConnection.java @@ -2,7 +2,6 @@ package com.alttd.chat.database; import com.alttd.chat.config.Config; -import com.alttd.chat.util.ALogger; import java.sql.Connection; import java.sql.DriverManager; @@ -66,6 +65,21 @@ public class DatabaseConnection { return connection; } + /** + * 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); + connection.setAutoCommit(false); + return connection; + } + /** * Sets the connection for this instance */ diff --git a/api/src/main/java/com/alttd/chat/database/Queries.java b/api/src/main/java/com/alttd/chat/database/Queries.java index 5d3df66..4c044ef 100755 --- a/api/src/main/java/com/alttd/chat/database/Queries.java +++ b/api/src/main/java/com/alttd/chat/database/Queries.java @@ -21,6 +21,7 @@ public class Queries { tables.add("CREATE TABLE IF NOT EXISTS mails (`id` INT NOT NULL AUTO_INCREMENT, `uuid` VARCHAR(36) NOT NULL, `sender` VARCHAR(36) NOT NULL, `message` VARCHAR(256) NOT NULL, `sendtime` BIGINT default 0, `readtime` BIGINT default 0, PRIMARY KEY (`id`))"); createNicknamesTable(); createRequestedNicknamesTable(); + ChatLogQueries.createChatLogTable(); try { Connection connection = DatabaseConnection.getConnection(); diff --git a/api/src/main/java/com/alttd/chat/objects/BatchInsertable.java b/api/src/main/java/com/alttd/chat/objects/BatchInsertable.java new file mode 100644 index 0000000..bde7fbd --- /dev/null +++ b/api/src/main/java/com/alttd/chat/objects/BatchInsertable.java @@ -0,0 +1,10 @@ +package com.alttd.chat.objects; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +public interface BatchInsertable { + + void prepareStatement(PreparedStatement preparedStatement) throws SQLException; + +} 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 new file mode 100644 index 0000000..8015b91 --- /dev/null +++ b/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLog.java @@ -0,0 +1,52 @@ +package com.alttd.chat.objects.chat_log; + +import com.alttd.chat.objects.BatchInsertable; +import org.jetbrains.annotations.NotNull; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.UUID; + +public class ChatLog implements BatchInsertable { + + private final UUID uuid; + private final Instant timestamp; + private final String server; + private final String message; + private final boolean blocked; + + protected ChatLog(UUID uuid, Instant timestamp, String server, String message, boolean blocked) { + this.uuid = uuid; + this.timestamp = timestamp; + this.server = server; + this.message = message; + this.blocked = blocked; + } + + @Override + public void prepareStatement(@NotNull PreparedStatement preparedStatement) throws SQLException { + preparedStatement.setString(1, uuid.toString()); + preparedStatement.setTimestamp(2, Timestamp.from(timestamp)); + preparedStatement.setString(3, server); + preparedStatement.setString(4, message); + preparedStatement.setInt(5, blocked ? 1 : 0); + } + + public UUID getUuid() { + return uuid; + } + + public Instant getTimestamp() { + return timestamp; + } + + public String getMessage() { + return message; + } + + public boolean isBlocked() { + return blocked; + } +} 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 new file mode 100644 index 0000000..09f0651 --- /dev/null +++ b/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLogHandler.java @@ -0,0 +1,112 @@ +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 it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import org.jetbrains.annotations.NotNull; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.UUID; +import java.util.concurrent.*; + +public class ChatLogHandler { + + private static ChatLogHandler instance = null; + private final ScheduledExecutorService executorService; + + public static ChatLogHandler getInstance() { + if (instance == null) + instance = new ChatLogHandler(); + return instance; + } + + private boolean isSaving; + private final Queue chatLogQueue = new ConcurrentLinkedQueue<>(); + private final Object2ObjectOpenHashMap> chatLogs = new Object2ObjectOpenHashMap<>(); + + public ChatLogHandler() { + Duration deleteThreshold = Duration.ofDays(Config.CHAT_LOG_DELETE_OLDER_THAN_DAYS); + ChatLogQueries.deleteOldMessages(deleteThreshold).thenAccept(success -> { + if (success) { + ALogger.info(String.format("Deleted all messages older than %s days from chat log database.", deleteThreshold.toDays())); + } else { + ALogger.warn(String.format("Failed to delete all messages older than %s days from chat log database.", deleteThreshold.toDays())); + } + }); + executorService = Executors.newSingleThreadScheduledExecutor(); + executorService.scheduleAtFixedRate(() -> saveToDatabase(false), + Config.CHAT_LOG_SAVE_DELAY_MINUTES, Config.CHAT_LOG_SAVE_DELAY_MINUTES, TimeUnit.MINUTES); + } + + public void shutDown() { + executorService.shutdown(); + saveToDatabase(true); + } + + private synchronized void savingToDatabase(boolean saving) { + isSaving = saving; + } + + private synchronized boolean isBlocked() { + return isSaving; + } + + public synchronized void addLog(ChatLog chatLog) { + if (isBlocked()) { + chatLogQueue.add(chatLog); + } else { + chatLogs.computeIfAbsent(chatLog.getUuid(), k -> new ArrayList<>()).add(chatLog); + } + } + + private void saveToDatabase(boolean onMainThread) { + savingToDatabase(true); + CompletableFuture booleanCompletableFuture = ChatLogQueries.storeMessages(chatLogs); + if (onMainThread) { + booleanCompletableFuture.join(); + return; + } + booleanCompletableFuture.whenComplete((result, throwable) -> { + if (throwable == null && result) { + chatLogs.clear(); + } + savingToDatabase(false); + while (!chatLogQueue.isEmpty()) { + addLog(chatLogQueue.remove()); + } + }); + } + + public 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 chatMessage = resultSet.getString("chat_message"); + boolean chatMessageBlocked = resultSet.getInt("blocked") == 1; + return new ChatLog(chatLogUUID, chatTimestamp, server, chatMessage, chatMessageBlocked); + } + + public void addChatLog(UUID uuid, String server, String message, boolean blocked) { + addLog(new ChatLog(uuid, Instant.now(), server, message, blocked)); + } + + public CompletableFuture> retrieveChatLogs(UUID uuid, Duration duration, String server) { + List chatLogList = chatLogs.getOrDefault(uuid, new ArrayList<>()); + return ChatLogQueries.retrieveMessages(this, uuid, duration, server) + .thenCompose(chatLogs -> CompletableFuture.supplyAsync(() -> { + chatLogList.addAll(chatLogs); + return chatLogList; + })) + .exceptionally(ex -> { + throw new CompletionException(ex); + }); + } + +} diff --git a/galaxy/src/main/java/com/alttd/chat/ChatPlugin.java b/galaxy/src/main/java/com/alttd/chat/ChatPlugin.java index 9122a63..6056d46 100755 --- a/galaxy/src/main/java/com/alttd/chat/ChatPlugin.java +++ b/galaxy/src/main/java/com/alttd/chat/ChatPlugin.java @@ -5,14 +5,12 @@ import com.alttd.chat.config.Config; import com.alttd.chat.config.ServerConfig; import com.alttd.chat.database.DatabaseConnection; import com.alttd.chat.handler.ChatHandler; -import com.alttd.chat.listeners.BookListener; -import com.alttd.chat.listeners.ChatListener; -import com.alttd.chat.listeners.PlayerListener; -import com.alttd.chat.listeners.PluginMessage; +import com.alttd.chat.listeners.*; import com.alttd.chat.nicknames.Nicknames; import com.alttd.chat.nicknames.NicknamesEvents; 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.util.ALogger; import com.alttd.chat.util.Utility; import org.bukkit.Bukkit; @@ -41,7 +39,8 @@ public class ChatPlugin extends JavaPlugin { chatHandler = new ChatHandler(); DatabaseConnection.initialize(); serverConfig = new ServerConfig(Bukkit.getServerName()); - registerListener(new PlayerListener(serverConfig), new ChatListener(), new BookListener()); + ChatLogHandler chatLogHandler = ChatLogHandler.getInstance(); + registerListener(new PlayerListener(serverConfig), new ChatListener(chatLogHandler), new BookListener(), new ShutdownListener(chatLogHandler)); if(serverConfig.GLOBALCHAT) { registerCommand("globalchat", new GlobalChat()); registerCommand("toggleglobalchat", new ToggleGlobalChat()); 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 6d9aa78..0db2434 100755 --- a/galaxy/src/main/java/com/alttd/chat/listeners/ChatListener.java +++ b/galaxy/src/main/java/com/alttd/chat/listeners/ChatListener.java @@ -6,6 +6,7 @@ import com.alttd.chat.handler.ChatHandler; 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.util.ALogger; import com.alttd.chat.util.GalaxyUtility; import com.alttd.chat.util.Utility; @@ -37,6 +38,11 @@ import java.util.stream.Collectors; public class ChatListener implements Listener { private final PlainTextComponentSerializer plainTextComponentSerializer = PlainTextComponentSerializer.plainText(); + private final ChatLogHandler chatLogHandler; + + public ChatListener(ChatLogHandler chatLogHandler) { + this.chatLogHandler = chatLogHandler; + } @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) @@ -102,6 +108,7 @@ public class ChatListener implements Listener { GalaxyUtility.sendBlockedNotification("Language", player, modifiableString.component(), ""); + chatLogHandler.addChatLog(player.getUniqueId(), player.getServer().getServerName(), PlainTextComponentSerializer.plainText().serialize(input), true); return; // the message was blocked } @@ -115,6 +122,7 @@ public class ChatListener implements Listener { for (Player pingPlayer : playersToPing) { pingPlayer.playSound(pingPlayer.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1, 1); } + chatLogHandler.addChatLog(player.getUniqueId(), player.getServer().getServerName(), modifiableString.string(), false); ALogger.info(PlainTextComponentSerializer.plainText().serialize(input)); } diff --git a/galaxy/src/main/java/com/alttd/chat/listeners/ShutdownListener.java b/galaxy/src/main/java/com/alttd/chat/listeners/ShutdownListener.java new file mode 100644 index 0000000..0c7ebd3 --- /dev/null +++ b/galaxy/src/main/java/com/alttd/chat/listeners/ShutdownListener.java @@ -0,0 +1,21 @@ +package com.alttd.chat.listeners; + +import com.alttd.chat.objects.chat_log.ChatLogHandler; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.server.PluginDisableEvent; + +public class ShutdownListener implements Listener { + + private final ChatLogHandler chatLogHandler; + + public ShutdownListener(ChatLogHandler chatLogHandler) { + this.chatLogHandler = chatLogHandler; + } + + @EventHandler + public void onShutdown(PluginDisableEvent event) { + chatLogHandler.shutDown(); + } + +}