From 44d6e994ccdfc44c508a991efaf9fdcaba32bc22 Mon Sep 17 00:00:00 2001 From: Teriuihi Date: Sat, 6 Apr 2024 17:25:15 +0200 Subject: [PATCH 01/15] Implement chat log handler with database support The code changes introduce the ability to log chat messages. A new ChatLogHandler class has been added that manages the queue of chat log messages, both storing them in memory and writing them to a database. New columns have been added to the database and the interactivity with the database is handled using prepared statements to improve security and performance. The chat messages are deleted from the database after a certain period, which can be configured. --- .../java/com/alttd/chat/config/Config.java | 8 ++ .../alttd/chat/database/ChatLogQueries.java | 102 ++++++++++++++++ .../chat/database/DatabaseConnection.java | 16 ++- .../java/com/alttd/chat/database/Queries.java | 1 + .../alttd/chat/objects/BatchInsertable.java | 10 ++ .../alttd/chat/objects/chat_log/ChatLog.java | 52 ++++++++ .../chat/objects/chat_log/ChatLogHandler.java | 112 ++++++++++++++++++ .../main/java/com/alttd/chat/ChatPlugin.java | 9 +- .../alttd/chat/listeners/ChatListener.java | 8 ++ .../chat/listeners/ShutdownListener.java | 21 ++++ 10 files changed, 333 insertions(+), 6 deletions(-) create mode 100644 api/src/main/java/com/alttd/chat/database/ChatLogQueries.java create mode 100644 api/src/main/java/com/alttd/chat/objects/BatchInsertable.java create mode 100644 api/src/main/java/com/alttd/chat/objects/chat_log/ChatLog.java create mode 100644 api/src/main/java/com/alttd/chat/objects/chat_log/ChatLogHandler.java create mode 100644 galaxy/src/main/java/com/alttd/chat/listeners/ShutdownListener.java 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(); + } + +} -- 2.39.5 From ed2ba7477280773b0c19ed38bb2785b755ee0e46 Mon Sep 17 00:00:00 2001 From: Teriuihi Date: Sat, 6 Apr 2024 19:39:36 +0200 Subject: [PATCH 02/15] Add start of VoteToMute functionality in chat system Implemented VoteToMute system enabling initiated voting for muting a player in chat. This includes creating new classes "ActiveVoteToMute", "VoteToMute", and "VoteToMuteStarter". The "VoteToMute" class handles the voting command logic, it allows players to vote on whether to mute other players. The code also adds a call to register this new command in the main VelocityChat class. --- .../com/alttd/velocitychat/VelocityChat.java | 1 + .../velocitychat/commands/VoteToMute.java | 160 ++++++++++++++++++ .../vote_to_mute/ActiveVoteToMute.java | 81 +++++++++ .../vote_to_mute/VoteToMuteStarter.java | 110 ++++++++++++ 4 files changed, 352 insertions(+) create mode 100644 velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMute.java create mode 100644 velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java create mode 100644 velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/VoteToMuteStarter.java diff --git a/velocity/src/main/java/com/alttd/velocitychat/VelocityChat.java b/velocity/src/main/java/com/alttd/velocitychat/VelocityChat.java index 6eeac13..44ed641 100755 --- a/velocity/src/main/java/com/alttd/velocitychat/VelocityChat.java +++ b/velocity/src/main/java/com/alttd/velocitychat/VelocityChat.java @@ -113,6 +113,7 @@ public class VelocityChat { new Reload(server); new MailCommand(server); new Report(server); + new VoteToMute(server); server.getCommandManager().register("party", new PartyCommand()); // all (proxy)commands go here } diff --git a/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMute.java b/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMute.java new file mode 100644 index 0000000..115bb73 --- /dev/null +++ b/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMute.java @@ -0,0 +1,160 @@ +package com.alttd.velocitychat.commands; + +import com.alttd.chat.config.Config; +import com.alttd.chat.util.Utility; +import com.alttd.velocitychat.commands.vote_to_mute.ActiveVoteToMute; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.tree.LiteralCommandNode; +import com.velocitypowered.api.command.BrigadierCommand; +import com.velocitypowered.api.command.CommandMeta; +import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ProxyServer; +import com.velocitypowered.api.proxy.ServerConnection; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +public class VoteToMute { + + public VoteToMute(ProxyServer proxyServer) { + RequiredArgumentBuilder playerNode = RequiredArgumentBuilder + .argument("player", StringArgumentType.string()) + .suggests((context, builder) -> { + List possiblePlayers; + if (context.getSource() instanceof Player player) { + Optional currentServer = player.getCurrentServer(); + if (currentServer.isPresent()) { + possiblePlayers = getEligiblePlayers(currentServer.get().getServer()); + } else { + possiblePlayers = getEligiblePlayers(proxyServer); + } + } else { + possiblePlayers = getEligiblePlayers(proxyServer); + } + Collection possibleValues = possiblePlayers.stream() + .map(Player::getUsername) + .toList(); + + if (possibleValues.isEmpty()) + return Suggestions.empty(); + + String remaining = builder.getRemaining().toLowerCase(); + possibleValues.stream() + .filter(str -> str.toLowerCase().startsWith(remaining)) + .map(StringArgumentType::escapeIfRequired) + .forEach(builder::suggest); + return builder.buildFuture(); + }) + .executes(context -> { + sendHelpMessage(context.getSource()); + return 1; + }); + + RequiredArgumentBuilder yesNoNode = RequiredArgumentBuilder. + argument("vote", StringArgumentType.string()) + .suggests(((commandContext, suggestionsBuilder) -> { + List yesNoValues = Arrays.asList("yes", "no"); + String remaining = suggestionsBuilder.getRemaining().toLowerCase(); + yesNoValues.stream() + .filter((String str) -> str.toLowerCase().startsWith(remaining)) + .map(StringArgumentType::escapeIfRequired) + .forEach(suggestionsBuilder::suggest); + return suggestionsBuilder.buildFuture(); + })); + + LiteralArgumentBuilder voteNode = LiteralArgumentBuilder + .literal("vote") + .then(playerNode + .then(yesNoNode + .executes(commandContext -> { + if (!(commandContext.getSource() instanceof Player)) { + commandContext.getSource().sendMessage(Utility.parseMiniMessage( + "Only players are allowed to vote")); + } + Player source = (Player) commandContext.getSource(); + String playerName = commandContext.getArgument("player", String.class); + Optional optionalActiveVoteToMute = ActiveVoteToMute.getInstance(playerName); + if (optionalActiveVoteToMute.isEmpty()) { + commandContext.getSource().sendMessage(Utility.parseMiniMessage( + "This player does not have an active vote to mute them")); + return 1; + } + ActiveVoteToMute activeVoteToMute = optionalActiveVoteToMute.get(); + String vote = commandContext.getArgument("vote", String.class); + switch (vote.toLowerCase()) { + case "yes" -> activeVoteToMute.vote(source.getUniqueId(), true); + case "no" -> activeVoteToMute.vote(source.getUniqueId(), false); + default -> commandContext.getSource().sendMessage(Utility.parseMiniMessage( + " is not a valid vote option", Placeholder.parsed("vote", vote))); + } + return 1; + })).executes(context -> { + sendHelpMessage(context.getSource()); + return 1; + })).executes(context -> { + sendHelpMessage(context.getSource()); + return 1; + }); + + LiteralCommandNode command = LiteralArgumentBuilder + .literal("votetomute") + .requires(commandSource -> commandSource.hasPermission("chat.vote-to-mute")) + .requires(commandSource -> commandSource instanceof Player) + .then(playerNode + .executes(commandContext -> { + String playerName = commandContext.getArgument("player", String.class); + Optional optionalPlayer = proxyServer.getPlayer(playerName); + if (optionalPlayer.isEmpty()) { + commandContext.getSource().sendMessage(Utility.parseMiniMessage( + "Player is not online", + Placeholder.parsed("player", playerName))); + return 1; + } + return 1; + })) + .then(voteNode) + .executes(context -> { + sendHelpMessage(context.getSource()); + return 1; + }) + .build(); + //TODO test command + //TODO add command to pick out the messages + //TODO add command to go to the next page + + BrigadierCommand brigadierCommand = new BrigadierCommand(command); + + CommandMeta.Builder metaBuilder = proxyServer.getCommandManager().metaBuilder(brigadierCommand); + + CommandMeta meta = metaBuilder.build(); + + proxyServer.getCommandManager().register(meta, brigadierCommand); + } + + private void sendHelpMessage(CommandSource commandSource) { + commandSource.sendMessage(Utility.parseMiniMessage("Use: /votetomute .")); + } + + private List getEligiblePlayers(ProxyServer proxyServer) { + return proxyServer.getAllPlayers().stream() + .filter(player -> player.hasPermission("chat.affected-by-vote-to-mute")) + .collect(Collectors.toList()); + } + + private List getEligiblePlayers(RegisteredServer registeredServer) { + return registeredServer.getPlayersConnected().stream() + .filter(player -> player.hasPermission("chat.affected-by-vote-to-mute")) + .collect(Collectors.toList()); + + } + +} diff --git a/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java new file mode 100644 index 0000000..47ef42f --- /dev/null +++ b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java @@ -0,0 +1,81 @@ +package com.alttd.velocitychat.commands.vote_to_mute; + +import com.alttd.chat.util.Utility; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import org.jetbrains.annotations.NotNull; + +import java.time.Duration; +import java.time.Instant; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Optional; +import java.util.UUID; + +public class ActiveVoteToMute { + + private static final HashMap instances = new HashMap<>(); + private static final Component prefix = Utility.parseMiniMessage("[VoteMute]"); + + private final Instant start; + private final Player votedPlayer; + private final Duration duration; + private HashSet votedFor = new HashSet<>(); + private HashSet votedAgainst = new HashSet<>(); + private int totalEligibleVoters; + + public static Optional getInstance(String username) { + if (!instances.containsKey(username)) + return Optional.empty(); + return Optional.of(instances.get(username)); + } + + public ActiveVoteToMute(@NotNull Player votedPlayer, Duration duration, int totalEligibleVoters) { + this.start = Instant.now(); + this.votedPlayer = votedPlayer; + this.duration = duration; + this.totalEligibleVoters = totalEligibleVoters; + instances.put(votedPlayer.getUsername(), this); + } + + public void start(@NotNull RegisteredServer registeredServer, Component chatLogs) { + Component message = Utility.parseMiniMessage( + String.format(""" + [VoteMute] A vote to mute for one hour has been started, please read the logs below before voting. + + Click: Mute --- Don't mute""", + votedPlayer.getUsername(), votedPlayer.getUsername()), + Placeholder.component("prefix", prefix), + Placeholder.parsed("player", votedPlayer.getUsername()), + Placeholder.component("logs", chatLogs)); + registeredServer.getPlayersConnected().stream() + .filter(player -> player.hasPermission("chat.vote-to-mute")) + .forEach(player -> player.sendMessage(message)); + } + + public void vote(UUID uuid, boolean votedToMute) { + if (votedToMute) { + votedFor.add(uuid); + votedAgainst.remove(uuid); + } else { + votedAgainst.add(uuid); + votedFor.remove(uuid); + } + } + + public boolean votePassed() { + double totalVotes = (votedFor.size() + votedAgainst.size()); + if (totalVotes / totalEligibleVoters < 0.6) { + return false; + } + return votedFor.size() / totalVotes > 0.6; + } + + public boolean voteEnded() { + if (votedFor.size() + votedAgainst.size() == totalEligibleVoters) + return true; + return duration.minus(Duration.between(start, Instant.now())).isNegative(); + } +} diff --git a/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/VoteToMuteStarter.java b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/VoteToMuteStarter.java new file mode 100644 index 0000000..dc2e04d --- /dev/null +++ b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/VoteToMuteStarter.java @@ -0,0 +1,110 @@ +package com.alttd.velocitychat.commands.vote_to_mute; + +import com.alttd.chat.objects.chat_log.ChatLog; +import com.alttd.chat.objects.chat_log.ChatLogHandler; +import com.alttd.chat.util.Utility; +import com.velocitypowered.api.proxy.Player; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.JoinConfiguration; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; + +import java.time.Duration; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class VoteToMuteStarter { + + private static final HashMap instanceMap = new HashMap<>(); + + private static final Component prefix = Utility.parseMiniMessage("[VoteMute]"); + private final ChatLogHandler chatLogHandler; + private final Player votedPlayer; + private final Player commandSource; + private final String serverName; + private List parsedChatLogs; + + public static Optional getInstance(UUID uuid) { + if (!instanceMap.containsKey(uuid)) + return Optional.empty(); + return Optional.of(instanceMap.get(uuid)); + } + + public VoteToMuteStarter(ChatLogHandler chatLogHandler, Player votedPlayer, Player commandSource, String serverName) { + this.chatLogHandler = chatLogHandler; + this.votedPlayer = votedPlayer; + this.commandSource = commandSource; + this.serverName = serverName; + instanceMap.put(commandSource.getUniqueId(), this); + } + + public void start() { + chatLogHandler.retrieveChatLogs(votedPlayer.getUniqueId(), Duration.ofMinutes(5), serverName).whenCompleteAsync((chatLogs, throwable) -> { + if (throwable != null) { + commandSource.sendMessage(Utility.parseMiniMessage(" Unable to retrieve messages for player ", + Placeholder.component("prefix", prefix), + Placeholder.parsed("player", votedPlayer.getUsername()))); + return; + } + parseChatLogs(chatLogs); + commandSource.sendMessage(Utility.parseMiniMessage( + " Please select up to 10 messages other players should see to decide their vote, seperated by comma's. " + + "Example: /votetomute messages 1, 2, 5, 8")); + showPage(0); + }); + } + + private void parseChatLogs(List chatLogs) { + TagResolver.Single playerTag = Placeholder.parsed("player", votedPlayer.getUsername()); + chatLogs.sort(Comparator.comparing(ChatLog::getTimestamp)); + parsedChatLogs = IntStream.range(0, chatLogs.size()) + .mapToObj(i -> Utility.parseMiniMessage( + ". [ChatLog] : ", + TagResolver.resolver( + Placeholder.unparsed("message", chatLogs.get(i).getMessage()), + Placeholder.parsed("number", String.valueOf(i + 1)), + playerTag + )) + ) + .toList(); + } + + public void showPage(int page) { + List collect = parsedChatLogs.stream().skip(page * 10L).limit(page).toList(); + Component chatLogsComponent = Component.join(JoinConfiguration.newlines(), collect); + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(" ChatLogs for \n\n"); + if (page != 0) { + stringBuilder.append("Click to go to previous page'> "); + } + if (parsedChatLogs.size() > page * 10) { + stringBuilder.append("Click to go to next page'> "); + } + commandSource.sendMessage(Utility.parseMiniMessage(stringBuilder.toString(), + Placeholder.parsed("player", votedPlayer.getUsername()), + Placeholder.component("prefix", prefix), + Placeholder.component("logs", chatLogsComponent))); + } + + /** + * Retrieves the chat logs for the given list of IDs. It removes 1 from the IDs before using them + * It removes the instance from the hashmap after this function call + * + * @param ids A list of integers representing the IDs of the chat logs to retrieve. + * @return A Component object containing the selected chat logs joined by newlines. + */ + public Component getChatLogsAndClose(List ids) { + List selectedChatLogs = ids.stream() + .filter(id -> id >= 1 && id <= parsedChatLogs.size()) + .map(id -> parsedChatLogs.get(id - 1)) + .collect(Collectors.toList()); + + instanceMap.remove(commandSource.getUniqueId()); + return Component.join(JoinConfiguration.newlines(), selectedChatLogs); + } +} -- 2.39.5 From 37aa9fdf4c11b43a8ca8bbebfeaf17ebb78f299e Mon Sep 17 00:00:00 2001 From: Teriuihi Date: Sun, 7 Apr 2024 16:32:33 +0200 Subject: [PATCH 03/15] Replace Object2ObjectOpenHashMap with HashMap The usage of Object2ObjectOpenHashMap in storing chat logs and the chat log handler was switched to HashMap. This was done to ensure the plugin can be run on proxy as velocity does not include this library --- .../main/java/com/alttd/chat/database/ChatLogQueries.java | 8 ++------ .../com/alttd/chat/objects/chat_log/ChatLogHandler.java | 8 ++------ 2 files changed, 4 insertions(+), 12 deletions(-) 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 ae3984d..a9da5d2 100644 --- a/api/src/main/java/com/alttd/chat/database/ChatLogQueries.java +++ b/api/src/main/java/com/alttd/chat/database/ChatLogQueries.java @@ -3,16 +3,12 @@ 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.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; @@ -34,7 +30,7 @@ public class ChatLogQueries { } } - public static @NotNull CompletableFuture storeMessages(Object2ObjectOpenHashMap> chatMessages) { + public static @NotNull CompletableFuture storeMessages(HashMap> chatMessages) { String insertQuery = "INSERT INTO chat_log (uuid, time_stamp, server, chat_message, blocked) VALUES (?, ?, ?, ?, ?)"; return CompletableFuture.supplyAsync(() -> { try (Connection connection = DatabaseConnection.createTransactionConnection()) { 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 09f0651..93fd6f5 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,17 +3,13 @@ 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.*; import java.util.concurrent.*; public class ChatLogHandler { @@ -29,7 +25,7 @@ public class ChatLogHandler { private boolean isSaving; private final Queue chatLogQueue = new ConcurrentLinkedQueue<>(); - private final Object2ObjectOpenHashMap> chatLogs = new Object2ObjectOpenHashMap<>(); + private final HashMap> chatLogs = new HashMap<>(); public ChatLogHandler() { Duration deleteThreshold = Duration.ofDays(Config.CHAT_LOG_DELETE_OLDER_THAN_DAYS); -- 2.39.5 From c25767e473390372213f29debfe042ac1cc65ed9 Mon Sep 17 00:00:00 2001 From: Teriuihi Date: Sun, 7 Apr 2024 18:07:20 +0200 Subject: [PATCH 04/15] Add VoteToMuteHelper and enhance ActiveVoteToMute Implemented a new module titled VoteToMuteHelper to enhance the voting system and augment the user experience. This module enhances the system by providing relevant player suggestions and setting up the mute player. Made updates to the ActiveVoteToMute module to handle potential voters and mute the player if the vote is passed. VoteToMute module is also updated to include the total eligible players. The code is made robust by adding appropriate error checks. --- .../com/alttd/velocitychat/VelocityChat.java | 6 +- .../velocitychat/commands/VoteToMute.java | 109 ++++---- .../commands/VoteToMuteHelper.java | 261 ++++++++++++++++++ .../vote_to_mute/ActiveVoteToMute.java | 140 ++++++++-- .../vote_to_mute/VoteToMuteStarter.java | 22 +- .../listeners/ProxyPlayerListener.java | 18 +- 6 files changed, 471 insertions(+), 85 deletions(-) create mode 100644 velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMuteHelper.java diff --git a/velocity/src/main/java/com/alttd/velocitychat/VelocityChat.java b/velocity/src/main/java/com/alttd/velocitychat/VelocityChat.java index 44ed641..a53c34e 100755 --- a/velocity/src/main/java/com/alttd/velocitychat/VelocityChat.java +++ b/velocity/src/main/java/com/alttd/velocitychat/VelocityChat.java @@ -5,6 +5,7 @@ import com.alttd.chat.ChatImplementation; 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.velocitychat.commands.*; import com.alttd.chat.config.Config; import com.alttd.chat.database.DatabaseConnection; @@ -23,6 +24,7 @@ import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; import com.velocitypowered.api.plugin.Dependency; import com.velocitypowered.api.plugin.Plugin; import com.velocitypowered.api.plugin.annotation.DataDirectory; +import com.velocitypowered.api.proxy.ConsoleCommandSource; import com.velocitypowered.api.proxy.ProxyServer; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; @@ -108,12 +110,14 @@ public class VelocityChat { } public void loadCommands() { + ChatLogHandler instance = ChatLogHandler.getInstance(); //TODO disable logging part new SilentJoinCommand(server); new GlobalAdminChat(server); new Reload(server); new MailCommand(server); new Report(server); - new VoteToMute(server); + new VoteToMute(server, instance); + new VoteToMuteHelper(server); server.getCommandManager().register("party", new PartyCommand()); // all (proxy)commands go here } diff --git a/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMute.java b/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMute.java index 115bb73..c6dc8e8 100644 --- a/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMute.java +++ b/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMute.java @@ -1,8 +1,8 @@ package com.alttd.velocitychat.commands; -import com.alttd.chat.config.Config; +import com.alttd.chat.objects.chat_log.ChatLogHandler; import com.alttd.chat.util.Utility; -import com.alttd.velocitychat.commands.vote_to_mute.ActiveVoteToMute; +import com.alttd.velocitychat.commands.vote_to_mute.VoteToMuteStarter; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.builder.RequiredArgumentBuilder; @@ -17,7 +17,6 @@ import com.velocitypowered.api.proxy.ServerConnection; import com.velocitypowered.api.proxy.server.RegisteredServer; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; -import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Optional; @@ -25,7 +24,7 @@ import java.util.stream.Collectors; public class VoteToMute { - public VoteToMute(ProxyServer proxyServer) { + public VoteToMute(ProxyServer proxyServer, ChatLogHandler chatLogHandler) { RequiredArgumentBuilder playerNode = RequiredArgumentBuilder .argument("player", StringArgumentType.string()) .suggests((context, builder) -> { @@ -59,77 +58,71 @@ public class VoteToMute { return 1; }); - RequiredArgumentBuilder yesNoNode = RequiredArgumentBuilder. - argument("vote", StringArgumentType.string()) - .suggests(((commandContext, suggestionsBuilder) -> { - List yesNoValues = Arrays.asList("yes", "no"); - String remaining = suggestionsBuilder.getRemaining().toLowerCase(); - yesNoValues.stream() - .filter((String str) -> str.toLowerCase().startsWith(remaining)) - .map(StringArgumentType::escapeIfRequired) - .forEach(suggestionsBuilder::suggest); - return suggestionsBuilder.buildFuture(); - })); - - LiteralArgumentBuilder voteNode = LiteralArgumentBuilder - .literal("vote") - .then(playerNode - .then(yesNoNode - .executes(commandContext -> { - if (!(commandContext.getSource() instanceof Player)) { - commandContext.getSource().sendMessage(Utility.parseMiniMessage( - "Only players are allowed to vote")); - } - Player source = (Player) commandContext.getSource(); - String playerName = commandContext.getArgument("player", String.class); - Optional optionalActiveVoteToMute = ActiveVoteToMute.getInstance(playerName); - if (optionalActiveVoteToMute.isEmpty()) { - commandContext.getSource().sendMessage(Utility.parseMiniMessage( - "This player does not have an active vote to mute them")); - return 1; - } - ActiveVoteToMute activeVoteToMute = optionalActiveVoteToMute.get(); - String vote = commandContext.getArgument("vote", String.class); - switch (vote.toLowerCase()) { - case "yes" -> activeVoteToMute.vote(source.getUniqueId(), true); - case "no" -> activeVoteToMute.vote(source.getUniqueId(), false); - default -> commandContext.getSource().sendMessage(Utility.parseMiniMessage( - " is not a valid vote option", Placeholder.parsed("vote", vote))); - } - return 1; - })).executes(context -> { - sendHelpMessage(context.getSource()); - return 1; - })).executes(context -> { - sendHelpMessage(context.getSource()); - return 1; - }); - LiteralCommandNode command = LiteralArgumentBuilder .literal("votetomute") .requires(commandSource -> commandSource.hasPermission("chat.vote-to-mute")) .requires(commandSource -> commandSource instanceof Player) .then(playerNode + .suggests(((commandContext, suggestionsBuilder) -> { + if (!(commandContext.getSource() instanceof Player player)) { + return suggestionsBuilder.buildFuture(); + } + Optional currentServer = player.getCurrentServer(); + if (currentServer.isEmpty()) { + sendHelpMessage(commandContext.getSource()); + return suggestionsBuilder.buildFuture(); + } + String remaining = suggestionsBuilder.getRemaining().toLowerCase(); + currentServer.get().getServer().getPlayersConnected().stream() + .filter(connectedPlayer -> connectedPlayer.hasPermission("chat.affected-by-vote-to-mute")) + .map(Player::getUsername) + .filter((String str) -> str.toLowerCase().startsWith(remaining)) + .map(StringArgumentType::escapeIfRequired) + .forEach(suggestionsBuilder::suggest); + return suggestionsBuilder.buildFuture(); + })) .executes(commandContext -> { String playerName = commandContext.getArgument("player", String.class); Optional optionalPlayer = proxyServer.getPlayer(playerName); if (optionalPlayer.isEmpty()) { commandContext.getSource().sendMessage(Utility.parseMiniMessage( - "Player is not online", + "Player is not online.", Placeholder.parsed("player", playerName))); return 1; } + Player voteTarget = optionalPlayer.get(); + if (!voteTarget.hasPermission("chat.affected-by-vote-to-mute")) { + commandContext.getSource().sendMessage(Utility.parseMiniMessage( + "Player can not be muted by a vote.", + Placeholder.parsed("player", playerName))); + return 1; + } + Player player = (Player) commandContext.getSource(); + Optional currentServer = player.getCurrentServer(); + if (currentServer.isEmpty()) { + sendHelpMessage(commandContext.getSource()); + return 1; + } + RegisteredServer server = currentServer.get().getServer(); + boolean countLowerRanks = false; + long count = getTotalEligiblePlayers(server, false); + if (count < 10) { + countLowerRanks = true; + count = getTotalEligiblePlayers(server, true); + if (count < 10) { + commandContext.getSource().sendMessage(Utility.parseMiniMessage("Not enough eligible players online to vote.")); + return 1; + } + } + new VoteToMuteStarter(chatLogHandler, voteTarget, player, server.getServerInfo().getName(), countLowerRanks) + .start(); return 1; })) - .then(voteNode) .executes(context -> { sendHelpMessage(context.getSource()); return 1; }) .build(); - //TODO test command - //TODO add command to pick out the messages - //TODO add command to go to the next page BrigadierCommand brigadierCommand = new BrigadierCommand(command); @@ -140,6 +133,12 @@ public class VoteToMute { proxyServer.getCommandManager().register(meta, brigadierCommand); } + private int getTotalEligiblePlayers(RegisteredServer server, boolean countLowerRanks) { + return (int) server.getPlayersConnected().stream() + .filter(player -> countLowerRanks ? player.hasPermission("chat.backup-vote-to-mute") : player.hasPermission("chat.vote-to-mute")) + .count(); + } + private void sendHelpMessage(CommandSource commandSource) { commandSource.sendMessage(Utility.parseMiniMessage("Use: /votetomute .")); } diff --git a/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMuteHelper.java b/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMuteHelper.java new file mode 100644 index 0000000..b2c6666 --- /dev/null +++ b/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMuteHelper.java @@ -0,0 +1,261 @@ +package com.alttd.velocitychat.commands; + +import com.alttd.chat.util.Utility; +import com.alttd.velocitychat.commands.vote_to_mute.ActiveVoteToMute; +import com.alttd.velocitychat.commands.vote_to_mute.VoteToMuteStarter; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.tree.LiteralCommandNode; +import com.velocitypowered.api.command.BrigadierCommand; +import com.velocitypowered.api.command.CommandMeta; +import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ProxyServer; +import com.velocitypowered.api.proxy.ServerConnection; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; + +import java.time.Duration; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class VoteToMuteHelper { + + public VoteToMuteHelper(ProxyServer proxyServer) { + RequiredArgumentBuilder playerNode = RequiredArgumentBuilder + .argument("player", StringArgumentType.string()) + .suggests((context, builder) -> { + List possiblePlayers; + if (context.getSource() instanceof Player player) { + Optional currentServer = player.getCurrentServer(); + if (currentServer.isPresent()) { + possiblePlayers = getEligiblePlayers(currentServer.get().getServer()); + } else { + possiblePlayers = getEligiblePlayers(proxyServer); + } + } else { + possiblePlayers = getEligiblePlayers(proxyServer); + } + Collection possibleValues = possiblePlayers.stream() + .map(Player::getUsername) + .toList(); + + if (possibleValues.isEmpty()) + return Suggestions.empty(); + + String remaining = builder.getRemaining().toLowerCase(); + possibleValues.stream() + .filter(str -> str.toLowerCase().startsWith(remaining)) + .map(StringArgumentType::escapeIfRequired) + .forEach(builder::suggest); + return builder.buildFuture(); + }) + .executes(context -> { + sendHelpMessage(context.getSource()); + return 1; + }); + + RequiredArgumentBuilder yesNoNode = RequiredArgumentBuilder. + argument("yesNo", StringArgumentType.string()) + .suggests(((commandContext, suggestionsBuilder) -> { + List yesNoValues = Arrays.asList("yes", "no"); + String remaining = suggestionsBuilder.getRemaining().toLowerCase(); + yesNoValues.stream() + .filter((String str) -> str.toLowerCase().startsWith(remaining)) + .map(StringArgumentType::escapeIfRequired) + .forEach(suggestionsBuilder::suggest); + return suggestionsBuilder.buildFuture(); + })); + + LiteralArgumentBuilder pageNode = LiteralArgumentBuilder + .literal("page") + .requires(commandSource -> commandSource.hasPermission("chat.vote-to-mute")) + .then(RequiredArgumentBuilder.argument("page number", IntegerArgumentType.integer(1)) + .suggests(((commandContext, suggestionsBuilder) -> { + if (!(commandContext.getSource() instanceof Player player)) { + return suggestionsBuilder.buildFuture(); + } + Optional instance = VoteToMuteStarter.getInstance(player.getUniqueId()); + if (instance.isEmpty()) { + return suggestionsBuilder.buildFuture(); + } + VoteToMuteStarter voteToMuteStarter = instance.get(); + String remaining = suggestionsBuilder.getRemaining().toLowerCase(); + int totalPages = voteToMuteStarter.getTotalPages(); + IntStream.range(1, totalPages + 1) + .mapToObj(String::valueOf) + .filter((String str) -> str.toLowerCase().startsWith(remaining)) + .map(StringArgumentType::escapeIfRequired) + .forEach(suggestionsBuilder::suggest); + return suggestionsBuilder.buildFuture(); + })) + .executes(commandContext -> { + if (!(commandContext.getSource() instanceof Player player)) { + commandContext.getSource().sendMessage(Utility.parseMiniMessage("Only players can use this command.")); + return 1; + } + Optional instance = VoteToMuteStarter.getInstance(player.getUniqueId()); + if (instance.isEmpty()) { + commandContext.getSource().sendMessage(Utility.parseMiniMessage("You don't have an active vote to mute.")); + return 1; + } + int pageNumber = commandContext.getArgument("page number", Integer.class); + instance.get().showPage(pageNumber); + return 1; + }) + ).executes(commandContext -> { + sendHelpMessage(commandContext.getSource()); + return 1; + }); + + LiteralArgumentBuilder enterPageNode = LiteralArgumentBuilder + .literal("messages") + .requires(commandSource -> commandSource.hasPermission("chat.vote-to-mute")) + .then(RequiredArgumentBuilder.argument("list of messages", StringArgumentType.greedyString()) + .executes(commandContext -> { + if (!(commandContext.getSource() instanceof Player player)) { + commandContext.getSource().sendMessage(Utility.parseMiniMessage("Only players can use this command.")); + return 1; + } + Optional instance = VoteToMuteStarter.getInstance(player.getUniqueId()); + if (instance.isEmpty()) { + commandContext.getSource().sendMessage(Utility.parseMiniMessage("You don't have an active vote to mute.")); + return 1; + } + String listOfPages = commandContext.getArgument("list of messages", String.class); + if (!listOfPages.matches("([1-9][0-9]*, )*[1-9][0-9]*")) { + commandContext.getSource().sendMessage(Utility.parseMiniMessage("Please make sure to format the command correctly.")); + return 1; + } + VoteToMuteStarter voteToMuteStarter = instance.get(); + + List collect = Arrays.stream(listOfPages.split(", ")) + .map(Integer::parseInt) + .collect(Collectors.toList()); + Optional max = collect.stream().max(Integer::compare); + if (max.isEmpty()) { + commandContext.getSource().sendMessage(Utility.parseMiniMessage("Some of your selected messages do not exist.")); + return 1; + } + int highestLogEntry = max.get(); + + if (voteToMuteStarter.getTotalLogEntries() > highestLogEntry) { + commandContext.getSource().sendMessage(Utility.parseMiniMessage("Some of your selected messages do not exist.")); + return 1; + } + + Optional currentServer = player.getCurrentServer(); + if (currentServer.isEmpty()) { + sendHelpMessage(commandContext.getSource()); + return 1; + } + + Component chatLogs = voteToMuteStarter.getChatLogsAndClose(collect); + RegisteredServer server = currentServer.get().getServer(); + long count = getTotalEligiblePlayers(server, voteToMuteStarter.countLowerRanks()); + new ActiveVoteToMute(voteToMuteStarter.getVotedPlayer(), server, proxyServer, Duration.ofMinutes(5), + (int) count, voteToMuteStarter.countLowerRanks(), chatLogs) + .start(); + return 1; + }) + ).executes(commandContext -> { + sendHelpMessage(commandContext.getSource()); + return 1; + }); + + LiteralArgumentBuilder voteNode = LiteralArgumentBuilder + .literal("vote") + .then(playerNode + .then(yesNoNode + .executes(commandContext -> { + if (!(commandContext.getSource() instanceof Player)) { + commandContext.getSource().sendMessage(Utility.parseMiniMessage( + "Only players are allowed to vote")); + } + Player source = (Player) commandContext.getSource(); + String playerName = commandContext.getArgument("player", String.class); + Optional optionalActiveVoteToMute = ActiveVoteToMute.getInstance(playerName); + if (optionalActiveVoteToMute.isEmpty()) { + commandContext.getSource().sendMessage(Utility.parseMiniMessage( + "This player does not have an active vote to mute them.")); + return 1; + } + ActiveVoteToMute activeVoteToMute = optionalActiveVoteToMute.get(); + + if (!activeVoteToMute.countLowerRanks()) { + if (!source.hasPermission("chat.vote-to-mute")) { + source.sendMessage(Utility.parseMiniMessage("You are not eligible to vote.")); + return 1; + } + } + + String vote = commandContext.getArgument("yesno", String.class); + switch (vote.toLowerCase()) { + case "yes" -> activeVoteToMute.vote(source.getUniqueId(), true); + case "no" -> activeVoteToMute.vote(source.getUniqueId(), false); + default -> commandContext.getSource().sendMessage(Utility.parseMiniMessage( + " is not a valid vote option", Placeholder.parsed("vote", vote))); + } + return 1; + })).executes(context -> { + sendHelpMessage(context.getSource()); + return 1; + })).executes(context -> { + sendHelpMessage(context.getSource()); + return 1; + }); + + LiteralCommandNode command = LiteralArgumentBuilder + .literal("votetomutehelper") + .requires(commandSource -> commandSource.hasPermission("chat.backup-vote-to-mute")) + .requires(commandSource -> commandSource instanceof Player) + .then(voteNode) + .then(pageNode) + .then(enterPageNode) + .executes(context -> { + sendHelpMessage(context.getSource()); + return 1; + }) + .build(); + + BrigadierCommand brigadierCommand = new BrigadierCommand(command); + + CommandMeta.Builder metaBuilder = proxyServer.getCommandManager().metaBuilder(brigadierCommand); + + CommandMeta meta = metaBuilder.build(); + + proxyServer.getCommandManager().register(meta, brigadierCommand); + } + + private int getTotalEligiblePlayers(RegisteredServer server, boolean countLowerRanks) { + return (int) server.getPlayersConnected().stream() + .filter(player -> countLowerRanks ? player.hasPermission("chat.backup-vote-to-mute") : player.hasPermission("chat.vote-to-mute")) + .count(); + } + + private void sendHelpMessage(CommandSource commandSource) { + commandSource.sendMessage(Utility.parseMiniMessage("Use: /votetomutehelper .")); + } + + private List getEligiblePlayers(ProxyServer proxyServer) { + return proxyServer.getAllPlayers().stream() + .filter(player -> player.hasPermission("chat.affected-by-vote-to-mute")) + .collect(Collectors.toList()); + } + + private List getEligiblePlayers(RegisteredServer registeredServer) { + return registeredServer.getPlayersConnected().stream() + .filter(player -> player.hasPermission("chat.affected-by-vote-to-mute")) + .collect(Collectors.toList()); + + } + +} diff --git a/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java index 47ef42f..bfb4c62 100644 --- a/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java +++ b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java @@ -2,29 +2,35 @@ package com.alttd.velocitychat.commands.vote_to_mute; import com.alttd.chat.util.Utility; import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ProxyServer; +import com.velocitypowered.api.proxy.ServerConnection; import com.velocitypowered.api.proxy.server.RegisteredServer; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; import org.jetbrains.annotations.NotNull; import java.time.Duration; -import java.time.Instant; import java.util.HashMap; import java.util.HashSet; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; public class ActiveVoteToMute { private static final HashMap instances = new HashMap<>(); private static final Component prefix = Utility.parseMiniMessage("[VoteMute]"); - private final Instant start; private final Player votedPlayer; - private final Duration duration; private HashSet votedFor = new HashSet<>(); private HashSet votedAgainst = new HashSet<>(); private int totalEligibleVoters; + private final boolean countLowerRanks; + private final RegisteredServer server; + private final ProxyServer proxyServer; + private final Component chatLogs; public static Optional getInstance(String username) { if (!instances.containsKey(username)) @@ -32,26 +38,71 @@ public class ActiveVoteToMute { return Optional.of(instances.get(username)); } - public ActiveVoteToMute(@NotNull Player votedPlayer, Duration duration, int totalEligibleVoters) { - this.start = Instant.now(); - this.votedPlayer = votedPlayer; - this.duration = duration; - this.totalEligibleVoters = totalEligibleVoters; - instances.put(votedPlayer.getUsername(), this); + public static void removePotentialVoter(Player player, RegisteredServer previousServer) { + if (!player.hasPermission("chat.backup-vote-to-mute")) + return; + if (player.hasPermission("chat.vote-to-mute")) { + instances.values().stream() + .filter(activeVoteToMute -> previousServer == null || activeVoteToMute.getServer().getServerInfo().hashCode() == previousServer.getServerInfo().hashCode()) + .forEach(inst -> inst.removeEligibleVoter(player.getUniqueId())); + } else { + instances.values().stream() + .filter(ActiveVoteToMute::countLowerRanks) + .filter(activeVoteToMute -> previousServer == null || activeVoteToMute.getServer().getServerInfo().hashCode() == previousServer.getServerInfo().hashCode()) + .forEach(inst -> inst.removeEligibleVoter(player.getUniqueId())); + } } - public void start(@NotNull RegisteredServer registeredServer, Component chatLogs) { - Component message = Utility.parseMiniMessage( - String.format(""" - [VoteMute] A vote to mute for one hour has been started, please read the logs below before voting. - - Click: Mute --- Don't mute""", - votedPlayer.getUsername(), votedPlayer.getUsername()), - Placeholder.component("prefix", prefix), - Placeholder.parsed("player", votedPlayer.getUsername()), - Placeholder.component("logs", chatLogs)); - registeredServer.getPlayersConnected().stream() - .filter(player -> player.hasPermission("chat.vote-to-mute")) + public static void addPotentialVoter(Player player, ServerConnection server) { + if (!player.hasPermission("chat.backup-vote-to-mute")) + return; + if (player.hasPermission("chat.vote-to-mute")) { + instances.values().stream() + .filter(activeVoteToMute -> activeVoteToMute.getServer().getServerInfo().hashCode() == server.getServerInfo().hashCode()) + .forEach(activeVoteToMute -> activeVoteToMute.addEligibleVoter(player)); + } else { + instances.values().stream() + .filter(ActiveVoteToMute::countLowerRanks) + .filter(activeVoteToMute -> activeVoteToMute.getServer().getServerInfo().hashCode() == server.getServerInfo().hashCode()) + .forEach(activeVoteToMute -> activeVoteToMute.addEligibleVoter(player)); + } + } + + public ActiveVoteToMute(@NotNull Player votedPlayer, @NotNull RegisteredServer server, ProxyServer proxyServer, Duration duration, + int totalEligibleVoters, boolean countLowerRanks, Component chatLogs) { + this.chatLogs = chatLogs; + this.votedPlayer = votedPlayer; + this.totalEligibleVoters = totalEligibleVoters; + this.countLowerRanks = countLowerRanks; + this.server = server; + this.proxyServer = proxyServer; + instances.put(votedPlayer.getUsername(), this); + ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); + executorService.schedule(this::endVote, + duration.toMinutes(), TimeUnit.MINUTES); + } + + private RegisteredServer getServer() { + return server; + } + + private void endVote() { + instances.remove(votedPlayer.getUsername()); + if (votePassed()) { + mutePlayer(); + return; + } + Component message = Utility.parseMiniMessage(" The vote to mute has failed, they will not be muted.", + Placeholder.component("prefix", prefix), Placeholder.parsed("player", votedPlayer.getUsername())); + server.getPlayersConnected().stream() + .filter(player -> countLowerRanks ? player.hasPermission("chat.backup-vote-to-mute") : player.hasPermission("chat.vote-to-mute")) + .forEach(player -> player.sendMessage(message)); + } + + public void start() { + Component message = getVoteStartMessage(); + server.getPlayersConnected().stream() + .filter(player -> countLowerRanks ? player.hasPermission("chat.backup-vote-to-mute") : player.hasPermission("chat.vote-to-mute")) .forEach(player -> player.sendMessage(message)); } @@ -59,6 +110,11 @@ public class ActiveVoteToMute { if (votedToMute) { votedFor.add(uuid); votedAgainst.remove(uuid); + if (!votePassed()) { + return; + } + instances.remove(votedPlayer.getUsername()); + mutePlayer(); } else { votedAgainst.add(uuid); votedFor.remove(uuid); @@ -73,9 +129,43 @@ public class ActiveVoteToMute { return votedFor.size() / totalVotes > 0.6; } - public boolean voteEnded() { - if (votedFor.size() + votedAgainst.size() == totalEligibleVoters) - return true; - return duration.minus(Duration.between(start, Instant.now())).isNegative(); + public boolean countLowerRanks() { + return countLowerRanks; + } + + private void mutePlayer() { + Component message = Utility.parseMiniMessage(" The vote to mute has passed, they will be muted.", + Placeholder.component("prefix", prefix), Placeholder.parsed("player", votedPlayer.getUsername())); + server.getPlayersConnected().stream() + .filter(player -> countLowerRanks ? player.hasPermission("chat.backup-vote-to-mute") : player.hasPermission("chat.vote-to-mute")) + .forEach(player -> player.sendMessage(message)); + proxyServer.getCommandManager().executeAsync(proxyServer.getConsoleCommandSource(), + String.format("tempmute %s 1h Muted by the community - under review.", votedPlayer.getUsername())); + } + + public void addEligibleVoter(Player player) { + UUID uuid = player.getUniqueId(); + if (votedAgainst.contains(uuid) || votedFor.contains(uuid)) + return; + totalEligibleVoters++; + player.sendMessage(getVoteStartMessage()); + } + + public void removeEligibleVoter(UUID uuid) { + if (votedFor.contains(uuid) || votedAgainst.contains(uuid)) + return; + totalEligibleVoters--; + } + + private Component getVoteStartMessage() { + return Utility.parseMiniMessage( + String.format(""" + [VoteMute] A vote to mute for one hour has been started, please read the logs below before voting. + + Click: Mute --- Don't mute""", + votedPlayer.getUsername(), votedPlayer.getUsername()), + Placeholder.component("prefix", prefix), + Placeholder.parsed("player", votedPlayer.getUsername()), + Placeholder.component("logs", chatLogs)); } } diff --git a/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/VoteToMuteStarter.java b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/VoteToMuteStarter.java index dc2e04d..d20c8af 100644 --- a/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/VoteToMuteStarter.java +++ b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/VoteToMuteStarter.java @@ -24,6 +24,7 @@ public class VoteToMuteStarter { private final Player commandSource; private final String serverName; private List parsedChatLogs; + private final boolean countLowerRanks; public static Optional getInstance(UUID uuid) { if (!instanceMap.containsKey(uuid)) @@ -31,11 +32,12 @@ public class VoteToMuteStarter { return Optional.of(instanceMap.get(uuid)); } - public VoteToMuteStarter(ChatLogHandler chatLogHandler, Player votedPlayer, Player commandSource, String serverName) { + public VoteToMuteStarter(ChatLogHandler chatLogHandler, Player votedPlayer, Player commandSource, String serverName, boolean countLowerRanks) { this.chatLogHandler = chatLogHandler; this.votedPlayer = votedPlayer; this.commandSource = commandSource; this.serverName = serverName; + this.countLowerRanks = countLowerRanks; instanceMap.put(commandSource.getUniqueId(), this); } @@ -50,7 +52,7 @@ public class VoteToMuteStarter { parseChatLogs(chatLogs); commandSource.sendMessage(Utility.parseMiniMessage( " Please select up to 10 messages other players should see to decide their vote, seperated by comma's. " + - "Example: /votetomute messages 1, 2, 5, 8")); + "Example: /votetomutehelper messages 1, 2, 5, 8")); showPage(0); }); } @@ -107,4 +109,20 @@ public class VoteToMuteStarter { instanceMap.remove(commandSource.getUniqueId()); return Component.join(JoinConfiguration.newlines(), selectedChatLogs); } + + public int getTotalPages() { + return (int) Math.ceil((double) parsedChatLogs.size() / 10); + } + + public Player getVotedPlayer() { + return votedPlayer; + } + + public int getTotalLogEntries() { + return parsedChatLogs.size(); + } + + public boolean countLowerRanks() { + return countLowerRanks; + } } diff --git a/velocity/src/main/java/com/alttd/velocitychat/listeners/ProxyPlayerListener.java b/velocity/src/main/java/com/alttd/velocitychat/listeners/ProxyPlayerListener.java index 091270f..f8c2f83 100755 --- a/velocity/src/main/java/com/alttd/velocitychat/listeners/ProxyPlayerListener.java +++ b/velocity/src/main/java/com/alttd/velocitychat/listeners/ProxyPlayerListener.java @@ -6,6 +6,7 @@ import com.alttd.chat.objects.Mail; import com.alttd.chat.util.Utility; import com.alttd.velocitychat.VelocityChat; import com.alttd.chat.config.Config; +import com.alttd.velocitychat.commands.vote_to_mute.ActiveVoteToMute; import com.alttd.velocitychat.data.ServerWrapper; import com.alttd.velocitychat.handlers.ServerHandler; import com.alttd.chat.managers.PartyManager; @@ -17,6 +18,7 @@ import com.velocitypowered.api.event.connection.LoginEvent; import com.velocitypowered.api.event.player.ServerConnectedEvent; import com.velocitypowered.api.event.player.ServerPostConnectEvent; import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ServerConnection; import com.velocitypowered.api.proxy.server.RegisteredServer; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; @@ -48,9 +50,20 @@ public class ProxyPlayerListener { @Subscribe(order = PostOrder.LAST) public void afterPlayerLogin(ServerPostConnectEvent event) { - if (event.getPreviousServer() != null) - return; Player player = event.getPlayer(); + RegisteredServer previousServer = event.getPreviousServer(); + if (previousServer != null) { + ActiveVoteToMute.removePotentialVoter(player, previousServer); + Optional currentServer = player.getCurrentServer(); + if (currentServer.isEmpty()) + return; + ActiveVoteToMute.addPotentialVoter(player, currentServer.get()); + return; + } + Optional currentServer = player.getCurrentServer(); + if (currentServer.isEmpty()) + return; + ActiveVoteToMute.addPotentialVoter(player, currentServer.get()); ChatUser chatUser = ChatUserManager.getChatUser(player.getUniqueId()); List unReadMail = chatUser.getUnReadMail(); if (unReadMail.isEmpty()) @@ -62,6 +75,7 @@ public class ProxyPlayerListener { @Subscribe public void quitEvent(DisconnectEvent event) { + ActiveVoteToMute.removePotentialVoter(event.getPlayer(), null); UUID uuid = event.getPlayer().getUniqueId(); Party party = PartyManager.getParty(event.getPlayer().getUniqueId()); if (party == null) return; -- 2.39.5 From c057c6965314e7c473c98f95fd7baa6e475d87e0 Mon Sep 17 00:00:00 2001 From: Teriuihi Date: Sun, 7 Apr 2024 18:11:19 +0200 Subject: [PATCH 05/15] Enable optional logging in ChatLogHandler Modified the ChatLogHandler to support optional logging by introducing a new argument in the getInstance() method. This argument sets the logging state during instantiation. This facilitates better flexibility when using the ChatLogHandler across different sections of the code base as logging requirements may differ. --- .../chat/objects/chat_log/ChatLogHandler.java | 14 ++++++++++---- .../src/main/java/com/alttd/chat/ChatPlugin.java | 2 +- .../java/com/alttd/velocitychat/VelocityChat.java | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) 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 93fd6f5..9eafa84 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 @@ -15,11 +15,11 @@ import java.util.concurrent.*; public class ChatLogHandler { private static ChatLogHandler instance = null; - private final ScheduledExecutorService executorService; + private ScheduledExecutorService executorService = null; - public static ChatLogHandler getInstance() { + public static ChatLogHandler getInstance(boolean enableLogging) { if (instance == null) - instance = new ChatLogHandler(); + instance = new ChatLogHandler(enableLogging); return instance; } @@ -27,7 +27,9 @@ public class ChatLogHandler { private final Queue chatLogQueue = new ConcurrentLinkedQueue<>(); private final HashMap> chatLogs = new HashMap<>(); - public ChatLogHandler() { + public ChatLogHandler(boolean enableLogging) { + if (!enableLogging) + return; Duration deleteThreshold = Duration.ofDays(Config.CHAT_LOG_DELETE_OLDER_THAN_DAYS); ChatLogQueries.deleteOldMessages(deleteThreshold).thenAccept(success -> { if (success) { @@ -41,6 +43,10 @@ public class ChatLogHandler { Config.CHAT_LOG_SAVE_DELAY_MINUTES, Config.CHAT_LOG_SAVE_DELAY_MINUTES, TimeUnit.MINUTES); } + /** + * Shuts down the executor service and saves the chat logs to the database. + * Will throw an error if called on a ChatLogHandler that was started without logging + */ public void shutDown() { executorService.shutdown(); saveToDatabase(true); diff --git a/galaxy/src/main/java/com/alttd/chat/ChatPlugin.java b/galaxy/src/main/java/com/alttd/chat/ChatPlugin.java index 6056d46..d56d317 100755 --- a/galaxy/src/main/java/com/alttd/chat/ChatPlugin.java +++ b/galaxy/src/main/java/com/alttd/chat/ChatPlugin.java @@ -39,7 +39,7 @@ public class ChatPlugin extends JavaPlugin { chatHandler = new ChatHandler(); DatabaseConnection.initialize(); serverConfig = new ServerConfig(Bukkit.getServerName()); - ChatLogHandler chatLogHandler = ChatLogHandler.getInstance(); + ChatLogHandler chatLogHandler = ChatLogHandler.getInstance(true); registerListener(new PlayerListener(serverConfig), new ChatListener(chatLogHandler), new BookListener(), new ShutdownListener(chatLogHandler)); if(serverConfig.GLOBALCHAT) { registerCommand("globalchat", new GlobalChat()); diff --git a/velocity/src/main/java/com/alttd/velocitychat/VelocityChat.java b/velocity/src/main/java/com/alttd/velocitychat/VelocityChat.java index a53c34e..d7cb3a9 100755 --- a/velocity/src/main/java/com/alttd/velocitychat/VelocityChat.java +++ b/velocity/src/main/java/com/alttd/velocitychat/VelocityChat.java @@ -110,7 +110,7 @@ public class VelocityChat { } public void loadCommands() { - ChatLogHandler instance = ChatLogHandler.getInstance(); //TODO disable logging part + ChatLogHandler instance = ChatLogHandler.getInstance(false); new SilentJoinCommand(server); new GlobalAdminChat(server); new Reload(server); -- 2.39.5 From d6135f2456e1d82b0f5d76d84e53c8486caa2827 Mon Sep 17 00:00:00 2001 From: Teriuihi Date: Sun, 7 Apr 2024 18:29:46 +0200 Subject: [PATCH 06/15] Add mute vote results sent to Discord and staff presence check Enhanced the vote-to-mute feature by adding a function that sends the mute vote results to a general channel on Discord. A 'staff presence' check has also been added which prevents the mute vote from being initiated if a staff member is online, instead, it prompts users to directly contact a staff member for help. --- .../velocitychat/commands/VoteToMute.java | 4 ++++ .../vote_to_mute/ActiveVoteToMute.java | 21 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMute.java b/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMute.java index c6dc8e8..872ef0b 100644 --- a/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMute.java +++ b/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMute.java @@ -104,6 +104,10 @@ public class VoteToMute { return 1; } RegisteredServer server = currentServer.get().getServer(); + if (currentServer.get().getServer().getPlayersConnected().stream().anyMatch(onlinePlayer -> onlinePlayer.hasPermission("chat.staff"))) { + commandContext.getSource().sendMessage(Utility.parseMiniMessage("There is a staff member online, so vote to mute can not be used. Please contact a staff member for help instead.")); + return 1; + } boolean countLowerRanks = false; long count = getTotalEligiblePlayers(server, false); if (count < 10) { diff --git a/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java index bfb4c62..83de93e 100644 --- a/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java +++ b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java @@ -1,14 +1,19 @@ package com.alttd.velocitychat.commands.vote_to_mute; +import com.alttd.chat.config.Config; import com.alttd.chat.util.Utility; +import com.alttd.proxydiscordlink.DiscordLink; +import com.alttd.proxydiscordlink.lib.net.dv8tion.jda.api.EmbedBuilder; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ProxyServer; import com.velocitypowered.api.proxy.ServerConnection; import com.velocitypowered.api.proxy.server.RegisteredServer; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.jetbrains.annotations.NotNull; +import java.awt.*; import java.time.Duration; import java.util.HashMap; import java.util.HashSet; @@ -141,6 +146,22 @@ public class ActiveVoteToMute { .forEach(player -> player.sendMessage(message)); proxyServer.getCommandManager().executeAsync(proxyServer.getConsoleCommandSource(), String.format("tempmute %s 1h Muted by the community - under review.", votedPlayer.getUsername())); + + + EmbedBuilder embedBuilder = new EmbedBuilder(); + embedBuilder.setAuthor(votedPlayer.getUsername(), null, "https://crafatar.com/avatars/" + votedPlayer.getUniqueId() + "?overlay"); + embedBuilder.setTitle("Player muted by vote"); + embedBuilder.setColor(Color.CYAN); + String chatLogsString = PlainTextComponentSerializer.plainText().serialize(chatLogs); + embedBuilder.addField("Logs", + chatLogsString.substring(0, Math.min(chatLogsString.length(), 1024)), + false); + embedBuilder.addField("Server", + server.getServerInfo().getName().substring(0, 1).toUpperCase() + server.getServerInfo().getName().substring(1), + false); + + long id = Config.serverChannelId.get("general"); + DiscordLink.getPlugin().getBot().sendEmbedToDiscord(id, embedBuilder, -1); } public void addEligibleVoter(Player player) { -- 2.39.5 From 192fca3a89ab24d6026db47477ee0e27832c39e6 Mon Sep 17 00:00:00 2001 From: Teriuihi Date: Sun, 7 Apr 2024 19:16:32 +0200 Subject: [PATCH 07/15] Fix chat log message deletion query Corrected the SQL query in the `deleteOldMessages` method within `ChatLogQueries.java`. Originally, it was incorrectly deleting newer messages rather than older ones due to an incorrect comparison symbol. It has now been adjusted to properly delete older messages based on the provided duration. --- api/src/main/java/com/alttd/chat/database/ChatLogQueries.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a9da5d2..1554450 100644 --- a/api/src/main/java/com/alttd/chat/database/ChatLogQueries.java +++ b/api/src/main/java/com/alttd/chat/database/ChatLogQueries.java @@ -82,7 +82,7 @@ public class ChatLogQueries { } public static CompletableFuture deleteOldMessages(Duration duration) { - String query = "DELETE FROM chat_log WHERE time_stamp > ?"; + String query = "DELETE FROM chat_log WHERE time_stamp < ?"; return CompletableFuture.supplyAsync(() -> { try (Connection connection = DatabaseConnection.getConnection()) { -- 2.39.5 From 3b2aa84164a2805af7b9e16a9f8cb9b3fca28c79 Mon Sep 17 00:00:00 2001 From: Teriuihi Date: Sat, 13 Apr 2024 16:52:56 +0200 Subject: [PATCH 08/15] Add player muted logging and abstract embed building The update introduces a log entry indicating when a player has been muted due to voting. The embed creation for this process has been isolated and extracted into a separate function. This contributes to better code modularity and organization. --- .../vote_to_mute/ActiveVoteToMute.java | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java index 83de93e..570438b 100644 --- a/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java +++ b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java @@ -1,6 +1,7 @@ package com.alttd.velocitychat.commands.vote_to_mute; import com.alttd.chat.config.Config; +import com.alttd.chat.util.ALogger; import com.alttd.chat.util.Utility; import com.alttd.proxydiscordlink.DiscordLink; import com.alttd.proxydiscordlink.lib.net.dv8tion.jda.api.EmbedBuilder; @@ -15,13 +16,12 @@ import org.jetbrains.annotations.NotNull; import java.awt.*; import java.time.Duration; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Optional; -import java.util.UUID; +import java.util.*; +import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; public class ActiveVoteToMute { @@ -148,20 +148,43 @@ public class ActiveVoteToMute { String.format("tempmute %s 1h Muted by the community - under review.", votedPlayer.getUsername())); + String chatLogsString = PlainTextComponentSerializer.plainText().serialize(chatLogs); + EmbedBuilder embedBuilder = buildMutedEmbed(chatLogsString); + + ALogger.info(String.format("Player %s muted by vote\nLogs:\n%s\n\nVotes for:\n%s\nVotes against:\n%s\n", + votedPlayer.getUsername(), + chatLogsString, + parseUUIDsToPlayerOrString(votedFor), + parseUUIDsToPlayerOrString(votedAgainst) + )); + + long id = Config.serverChannelId.get("general"); + DiscordLink.getPlugin().getBot().sendEmbedToDiscord(id, embedBuilder, -1); + } + + @NotNull + private EmbedBuilder buildMutedEmbed(String chatLogsString) { EmbedBuilder embedBuilder = new EmbedBuilder(); embedBuilder.setAuthor(votedPlayer.getUsername(), null, "https://crafatar.com/avatars/" + votedPlayer.getUniqueId() + "?overlay"); embedBuilder.setTitle("Player muted by vote"); embedBuilder.setColor(Color.CYAN); - String chatLogsString = PlainTextComponentSerializer.plainText().serialize(chatLogs); embedBuilder.addField("Logs", chatLogsString.substring(0, Math.min(chatLogsString.length(), 1024)), false); embedBuilder.addField("Server", server.getServerInfo().getName().substring(0, 1).toUpperCase() + server.getServerInfo().getName().substring(1), false); + return embedBuilder; + } - long id = Config.serverChannelId.get("general"); - DiscordLink.getPlugin().getBot().sendEmbedToDiscord(id, embedBuilder, -1); + private String parseUUIDsToPlayerOrString(Collection uuids) { + return uuids.stream().map(uuid -> { + Optional player = proxyServer.getPlayer(uuid); + if (player.isPresent()) { + return player.get().getUsername(); + } + return uuid.toString(); + }).collect(Collectors.joining("\n")); } public void addEligibleVoter(Player player) { -- 2.39.5 From 187f71d6c3dc73252eb9755a1b4e724f2f303968 Mon Sep 17 00:00:00 2001 From: Teriuihi Date: Sun, 28 Apr 2024 21:36:45 +0200 Subject: [PATCH 09/15] Implement logging for ChatLogHandler The ChatLogHandler now includes logging for better troubleshooting and understanding of the server state. Logging triggers when chat logging is disabled, when it starts, and also if there's a failure in saving chat messages to the database. --- .../com/alttd/chat/objects/chat_log/ChatLogHandler.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 9eafa84..d226a36 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 @@ -28,8 +28,10 @@ public class ChatLogHandler { private final HashMap> chatLogs = new HashMap<>(); public ChatLogHandler(boolean enableLogging) { - if (!enableLogging) + if (!enableLogging) { + ALogger.info("Logging is not enabled on this server."); return; + } Duration deleteThreshold = Duration.ofDays(Config.CHAT_LOG_DELETE_OLDER_THAN_DAYS); ChatLogQueries.deleteOldMessages(deleteThreshold).thenAccept(success -> { if (success) { @@ -41,6 +43,7 @@ public class ChatLogHandler { executorService = Executors.newSingleThreadScheduledExecutor(); executorService.scheduleAtFixedRate(() -> saveToDatabase(false), Config.CHAT_LOG_SAVE_DELAY_MINUTES, Config.CHAT_LOG_SAVE_DELAY_MINUTES, TimeUnit.MINUTES); + ALogger.info("Logging started!"); } /** @@ -78,6 +81,8 @@ public class ChatLogHandler { booleanCompletableFuture.whenComplete((result, throwable) -> { if (throwable == null && result) { chatLogs.clear(); + } else { + ALogger.error("Failed to save chat messages."); } savingToDatabase(false); while (!chatLogQueue.isEmpty()) { -- 2.39.5 From 65503a02f37f77045a5835a07152d883604f7e74 Mon Sep 17 00:00:00 2001 From: Teriuihi Date: Tue, 30 Apr 2024 22:15:46 +0200 Subject: [PATCH 10/15] Refactor death message display in PlayerListener The commit refactors the method for displaying player death messages in the PlayerListener class. Specifically, it adds functionality to replace usernames with display names in death notifications. More descriptive death messages with themed colors and icons are now shown. --- .../alttd/chat/listeners/PlayerListener.java | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/galaxy/src/main/java/com/alttd/chat/listeners/PlayerListener.java b/galaxy/src/main/java/com/alttd/chat/listeners/PlayerListener.java index ac993f0..ef173ca 100755 --- a/galaxy/src/main/java/com/alttd/chat/listeners/PlayerListener.java +++ b/galaxy/src/main/java/com/alttd/chat/listeners/PlayerListener.java @@ -11,6 +11,7 @@ import com.alttd.chat.objects.Toggleable; import com.alttd.chat.util.GalaxyUtility; import com.alttd.chat.util.Utility; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextReplacementConfig; import net.kyori.adventure.text.format.Style; import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.format.TextDecoration; @@ -110,15 +111,29 @@ public class PlayerListener implements Listener { if (playerDeathsStack.size() > Config.DEATH_MESSAGES_MAX_PER_PERIOD || serverConfig.MUTED) { event.deathMessage(Component.empty()); return; - } else { - Component component = event.deathMessage(); - if (component != null) { - component = Component.text("* ").append(component); - component = component.style(Style.style(TextColor.color(82, 80, 77), TextDecoration.ITALIC)); - event.deathMessage(component); - } } + Component component = event.deathMessage(); + playerDeathsStack.push(Instant.now()); + if (component == null) { + return; + } + TextReplacementConfig playerReplacement = TextReplacementConfig.builder() + .match(event.getPlayer().getName()) + .replacement(event.getPlayer().displayName()) + .build(); + component = component.replaceText(playerReplacement); + Player killer = event.getPlayer().getKiller(); + if (killer != null) { + TextReplacementConfig killerReplacement = TextReplacementConfig.builder() + .match(killer.getName()) + .replacement(killer.displayName()) + .build(); + component = component.replaceText(killerReplacement); + } + component = MiniMessage.miniMessage().deserialize("[] ").append(component); + component = component.style(Style.style(TextColor.color(255, 155, 48), TextDecoration.ITALIC)); + event.deathMessage(component); } } -- 2.39.5 From f3147b3256b888981fc5de1640a2888ef989fca8 Mon Sep 17 00:00:00 2001 From: Teriuihi Date: Sat, 4 May 2024 23:41:27 +0200 Subject: [PATCH 11/15] Refine ChatPlugin and improve "vote to mute" logic Modified ChatPlugin to include "thisPlugin" within the ShutdownListener initialization. Additionally, adjusted the mute vote failure message color from green to red in ActiveVoteToMute file and added a condition to return false if there are no votes. Also, made improvements to the pagination logic in the VoteToMuteStarter file. Lastly, improved the chat logging mechanics in ChatLogHandler by adding and refining various log information for capturing action details. --- .../alttd/chat/objects/chat_log/ChatLogHandler.java | 13 +++++++++++-- galaxy/src/main/java/com/alttd/chat/ChatPlugin.java | 2 +- .../com/alttd/chat/listeners/ShutdownListener.java | 8 +++++++- .../commands/vote_to_mute/ActiveVoteToMute.java | 8 +++++--- .../commands/vote_to_mute/VoteToMuteStarter.java | 10 +++++----- 5 files changed, 29 insertions(+), 12 deletions(-) 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 d226a36..a3fee53 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 @@ -41,9 +41,12 @@ public class ChatLogHandler { } }); executorService = Executors.newSingleThreadScheduledExecutor(); - executorService.scheduleAtFixedRate(() -> saveToDatabase(false), + executorService.scheduleAtFixedRate(() -> { + saveToDatabase(false); + ALogger.info(String.format("Running scheduler to save messages with a %d delay", Config.CHAT_LOG_SAVE_DELAY_MINUTES)); + }, Config.CHAT_LOG_SAVE_DELAY_MINUTES, Config.CHAT_LOG_SAVE_DELAY_MINUTES, TimeUnit.MINUTES); - ALogger.info("Logging started!"); + ALogger.info("Logging has started!"); } /** @@ -73,9 +76,11 @@ public class ChatLogHandler { private void saveToDatabase(boolean onMainThread) { savingToDatabase(true); + ALogger.info(String.format("Saving %d messages to database", chatLogs.size())); CompletableFuture booleanCompletableFuture = ChatLogQueries.storeMessages(chatLogs); if (onMainThread) { booleanCompletableFuture.join(); + ALogger.info("Finished saving messages on main thread"); return; } booleanCompletableFuture.whenComplete((result, throwable) -> { @@ -85,9 +90,13 @@ public class ChatLogHandler { ALogger.error("Failed to save chat messages."); } savingToDatabase(false); + if (!chatLogQueue.isEmpty()) { + ALogger.info("Adding back messages from queue to chatLogs map"); + } while (!chatLogQueue.isEmpty()) { addLog(chatLogQueue.remove()); } + ALogger.info("Finished saving messages"); }); } diff --git a/galaxy/src/main/java/com/alttd/chat/ChatPlugin.java b/galaxy/src/main/java/com/alttd/chat/ChatPlugin.java index d56d317..9721038 100755 --- a/galaxy/src/main/java/com/alttd/chat/ChatPlugin.java +++ b/galaxy/src/main/java/com/alttd/chat/ChatPlugin.java @@ -40,7 +40,7 @@ public class ChatPlugin extends JavaPlugin { DatabaseConnection.initialize(); serverConfig = new ServerConfig(Bukkit.getServerName()); ChatLogHandler chatLogHandler = ChatLogHandler.getInstance(true); - registerListener(new PlayerListener(serverConfig), new ChatListener(chatLogHandler), new BookListener(), new ShutdownListener(chatLogHandler)); + registerListener(new PlayerListener(serverConfig), new ChatListener(chatLogHandler), new BookListener(), new ShutdownListener(chatLogHandler, this)); if(serverConfig.GLOBALCHAT) { registerCommand("globalchat", new GlobalChat()); registerCommand("toggleglobalchat", new ToggleGlobalChat()); diff --git a/galaxy/src/main/java/com/alttd/chat/listeners/ShutdownListener.java b/galaxy/src/main/java/com/alttd/chat/listeners/ShutdownListener.java index 0c7ebd3..6b4dfc0 100644 --- a/galaxy/src/main/java/com/alttd/chat/listeners/ShutdownListener.java +++ b/galaxy/src/main/java/com/alttd/chat/listeners/ShutdownListener.java @@ -4,17 +4,23 @@ import com.alttd.chat.objects.chat_log.ChatLogHandler; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.server.PluginDisableEvent; +import org.bukkit.plugin.Plugin; public class ShutdownListener implements Listener { private final ChatLogHandler chatLogHandler; + private final Plugin thisPlugin; - public ShutdownListener(ChatLogHandler chatLogHandler) { + public ShutdownListener(ChatLogHandler chatLogHandler, Plugin thisPlugin) { this.chatLogHandler = chatLogHandler; + this.thisPlugin = thisPlugin; } @EventHandler public void onShutdown(PluginDisableEvent event) { + if (!event.getPlugin().getName().equals(thisPlugin.getName())){ + return; + } chatLogHandler.shutDown(); } diff --git a/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java index 570438b..cb427bc 100644 --- a/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java +++ b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java @@ -17,7 +17,6 @@ import org.jetbrains.annotations.NotNull; import java.awt.*; import java.time.Duration; import java.util.*; -import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -97,7 +96,7 @@ public class ActiveVoteToMute { mutePlayer(); return; } - Component message = Utility.parseMiniMessage(" The vote to mute has failed, they will not be muted.", + Component message = Utility.parseMiniMessage(" The vote to mute has failed, they will not be muted.", Placeholder.component("prefix", prefix), Placeholder.parsed("player", votedPlayer.getUsername())); server.getPlayersConnected().stream() .filter(player -> countLowerRanks ? player.hasPermission("chat.backup-vote-to-mute") : player.hasPermission("chat.vote-to-mute")) @@ -128,7 +127,10 @@ public class ActiveVoteToMute { public boolean votePassed() { double totalVotes = (votedFor.size() + votedAgainst.size()); - if (totalVotes / totalEligibleVoters < 0.6) { + if (totalVotes == 0) { + return false; + } + if (totalVotes / totalEligibleVoters < 0.6) { return false; } return votedFor.size() / totalVotes > 0.6; diff --git a/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/VoteToMuteStarter.java b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/VoteToMuteStarter.java index d20c8af..b08927e 100644 --- a/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/VoteToMuteStarter.java +++ b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/VoteToMuteStarter.java @@ -73,19 +73,19 @@ public class VoteToMuteStarter { } public void showPage(int page) { - List collect = parsedChatLogs.stream().skip(page * 10L).limit(page).toList(); + List collect = parsedChatLogs.stream().skip((page - 1) * 10L).limit(10L).toList(); Component chatLogsComponent = Component.join(JoinConfiguration.newlines(), collect); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(" ChatLogs for \n\n"); - if (page != 0) { - stringBuilder.append(" 1) { + stringBuilder.append("Click to go to previous page'> "); } if (parsedChatLogs.size() > page * 10) { - stringBuilder.append("Click to go to next page'> "); + .append(">Click to go to next page'> "); } commandSource.sendMessage(Utility.parseMiniMessage(stringBuilder.toString(), Placeholder.parsed("player", votedPlayer.getUsername()), -- 2.39.5 From a876a9f77b81254ec36e9b3ebadbdf62a059b78b Mon Sep 17 00:00:00 2001 From: Teriuihi Date: Sun, 5 May 2024 00:11:56 +0200 Subject: [PATCH 12/15] Update vote validation and argument naming Updated the method "votePassed" in "ActiveVoteToMute" to consider a scenario where no votes have been made for muting. Also, corrected the argument name from "yesno" to "yesNo" in "VoteToMuteHelper" to match with its name in the command constructor --- .../java/com/alttd/velocitychat/commands/VoteToMuteHelper.java | 2 +- .../velocitychat/commands/vote_to_mute/ActiveVoteToMute.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMuteHelper.java b/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMuteHelper.java index b2c6666..34effdc 100644 --- a/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMuteHelper.java +++ b/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMuteHelper.java @@ -197,7 +197,7 @@ public class VoteToMuteHelper { } } - String vote = commandContext.getArgument("yesno", String.class); + String vote = commandContext.getArgument("yesNo", String.class); switch (vote.toLowerCase()) { case "yes" -> activeVoteToMute.vote(source.getUniqueId(), true); case "no" -> activeVoteToMute.vote(source.getUniqueId(), false); diff --git a/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java index cb427bc..4448a50 100644 --- a/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java +++ b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java @@ -127,7 +127,7 @@ public class ActiveVoteToMute { public boolean votePassed() { double totalVotes = (votedFor.size() + votedAgainst.size()); - if (totalVotes == 0) { + if (totalVotes == 0 || votedFor.isEmpty()) { return false; } if (totalVotes / totalEligibleVoters < 0.6) { -- 2.39.5 From 782c7df4ef3104dcc77b311dc44dca3189bf0483 Mon Sep 17 00:00:00 2001 From: Teriuihi Date: Sun, 5 May 2024 15:42:59 +0200 Subject: [PATCH 13/15] Update vote to mute functionality in chat system Several changes were made to update the vote to mute functionality in the chat system. The threshold for eligible players online for vote has been changed from 10 to 6. In addition, improvements have been made to prevent the vote from ending prematurely. Lastly, feedback messages to users when they cast their vote and an update to the vote start message format have been implemented. --- .../alttd/velocitychat/commands/VoteToMute.java | 4 ++-- .../velocitychat/commands/VoteToMuteHelper.java | 16 ++++++++++++---- .../commands/vote_to_mute/ActiveVoteToMute.java | 6 +++++- .../commands/vote_to_mute/VoteToMuteStarter.java | 4 ++-- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMute.java b/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMute.java index 872ef0b..7a7e260 100644 --- a/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMute.java +++ b/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMute.java @@ -110,10 +110,10 @@ public class VoteToMute { } boolean countLowerRanks = false; long count = getTotalEligiblePlayers(server, false); - if (count < 10) { + if (count < 6) { countLowerRanks = true; count = getTotalEligiblePlayers(server, true); - if (count < 10) { + if (count < 6) { commandContext.getSource().sendMessage(Utility.parseMiniMessage("Not enough eligible players online to vote.")); return 1; } diff --git a/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMuteHelper.java b/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMuteHelper.java index 34effdc..0b26984 100644 --- a/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMuteHelper.java +++ b/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMuteHelper.java @@ -116,7 +116,7 @@ public class VoteToMuteHelper { return 1; }); - LiteralArgumentBuilder enterPageNode = LiteralArgumentBuilder + LiteralArgumentBuilder enterMessagesNode = LiteralArgumentBuilder .literal("messages") .requires(commandSource -> commandSource.hasPermission("chat.vote-to-mute")) .then(RequiredArgumentBuilder.argument("list of messages", StringArgumentType.greedyString()) @@ -199,8 +199,16 @@ public class VoteToMuteHelper { String vote = commandContext.getArgument("yesNo", String.class); switch (vote.toLowerCase()) { - case "yes" -> activeVoteToMute.vote(source.getUniqueId(), true); - case "no" -> activeVoteToMute.vote(source.getUniqueId(), false); + case "yes" -> { + activeVoteToMute.vote(source.getUniqueId(), true); + commandContext.getSource().sendMessage(Utility.parseMiniMessage( + "You voted to mute. Thanks for voting, staff will be online soon to review!")); + } + case "no" -> { + activeVoteToMute.vote(source.getUniqueId(), false); + commandContext.getSource().sendMessage(Utility.parseMiniMessage( + "You voted not to mute. Thanks for voting, staff will be online soon to review!")); + } default -> commandContext.getSource().sendMessage(Utility.parseMiniMessage( " is not a valid vote option", Placeholder.parsed("vote", vote))); } @@ -219,7 +227,7 @@ public class VoteToMuteHelper { .requires(commandSource -> commandSource instanceof Player) .then(voteNode) .then(pageNode) - .then(enterPageNode) + .then(enterMessagesNode) .executes(context -> { sendHelpMessage(context.getSource()); return 1; diff --git a/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java index 4448a50..9d629f1 100644 --- a/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java +++ b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java @@ -35,6 +35,7 @@ public class ActiveVoteToMute { private final RegisteredServer server; private final ProxyServer proxyServer; private final Component chatLogs; + private boolean endedVote = false; public static Optional getInstance(String username) { if (!instances.containsKey(username)) @@ -91,6 +92,8 @@ public class ActiveVoteToMute { } private void endVote() { + if (endedVote) + return; instances.remove(votedPlayer.getUsername()); if (votePassed()) { mutePlayer(); @@ -117,6 +120,7 @@ public class ActiveVoteToMute { if (!votePassed()) { return; } + endedVote = true; instances.remove(votedPlayer.getUsername()); mutePlayer(); } else { @@ -206,7 +210,7 @@ public class ActiveVoteToMute { private Component getVoteStartMessage() { return Utility.parseMiniMessage( String.format(""" - [VoteMute] A vote to mute for one hour has been started, please read the logs below before voting. + A vote to mute for one hour has been started, please read the logs below before voting. Click: Mute --- Don't mute""", votedPlayer.getUsername(), votedPlayer.getUsername()), diff --git a/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/VoteToMuteStarter.java b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/VoteToMuteStarter.java index b08927e..66ea87e 100644 --- a/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/VoteToMuteStarter.java +++ b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/VoteToMuteStarter.java @@ -52,8 +52,8 @@ public class VoteToMuteStarter { parseChatLogs(chatLogs); commandSource.sendMessage(Utility.parseMiniMessage( " Please select up to 10 messages other players should see to decide their vote, seperated by comma's. " + - "Example: /votetomutehelper messages 1, 2, 5, 8")); - showPage(0); + "Example: /votetomutehelper messages 1, 2, 5, 8", Placeholder.component("prefix", prefix))); + showPage(1); }); } -- 2.39.5 From 8393d12c6d5f888404c5f0206af324a342e519e0 Mon Sep 17 00:00:00 2001 From: Teriuihi Date: Sun, 5 May 2024 16:14:48 +0200 Subject: [PATCH 14/15] Update vote-to-mute feature The vote-to-mute feature is updated to include information about the vote initiating player. Also, the duration to retrieve chat logs increased from 5 minutes to 10 minutes. Lastly, eligible players are now notified live about the voting progress. --- .../commands/VoteToMuteHelper.java | 30 ++++++++++++++----- .../vote_to_mute/ActiveVoteToMute.java | 23 ++++++++++++-- .../vote_to_mute/VoteToMuteStarter.java | 7 +++-- 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMuteHelper.java b/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMuteHelper.java index 0b26984..f646c81 100644 --- a/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMuteHelper.java +++ b/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMuteHelper.java @@ -16,6 +16,7 @@ import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ProxyServer; import com.velocitypowered.api.proxy.ServerConnection; import com.velocitypowered.api.proxy.server.RegisteredServer; +import jdk.jshell.execution.Util; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; @@ -29,6 +30,8 @@ import java.util.stream.IntStream; public class VoteToMuteHelper { + private static final Component prefix = Utility.parseMiniMessage("[VoteMute]"); + public VoteToMuteHelper(ProxyServer proxyServer) { RequiredArgumentBuilder playerNode = RequiredArgumentBuilder .argument("player", StringArgumentType.string()) @@ -162,7 +165,7 @@ public class VoteToMuteHelper { RegisteredServer server = currentServer.get().getServer(); long count = getTotalEligiblePlayers(server, voteToMuteStarter.countLowerRanks()); new ActiveVoteToMute(voteToMuteStarter.getVotedPlayer(), server, proxyServer, Duration.ofMinutes(5), - (int) count, voteToMuteStarter.countLowerRanks(), chatLogs) + (int) count, voteToMuteStarter.countLowerRanks(), chatLogs, player) .start(); return 1; }) @@ -176,11 +179,11 @@ public class VoteToMuteHelper { .then(playerNode .then(yesNoNode .executes(commandContext -> { - if (!(commandContext.getSource() instanceof Player)) { + if (!(commandContext.getSource() instanceof Player player)) { commandContext.getSource().sendMessage(Utility.parseMiniMessage( "Only players are allowed to vote")); + return 1; } - Player source = (Player) commandContext.getSource(); String playerName = commandContext.getArgument("player", String.class); Optional optionalActiveVoteToMute = ActiveVoteToMute.getInstance(playerName); if (optionalActiveVoteToMute.isEmpty()) { @@ -191,8 +194,8 @@ public class VoteToMuteHelper { ActiveVoteToMute activeVoteToMute = optionalActiveVoteToMute.get(); if (!activeVoteToMute.countLowerRanks()) { - if (!source.hasPermission("chat.vote-to-mute")) { - source.sendMessage(Utility.parseMiniMessage("You are not eligible to vote.")); + if (!player.hasPermission("chat.vote-to-mute")) { + player.sendMessage(Utility.parseMiniMessage("You are not eligible to vote.")); return 1; } } @@ -200,12 +203,13 @@ public class VoteToMuteHelper { String vote = commandContext.getArgument("yesNo", String.class); switch (vote.toLowerCase()) { case "yes" -> { - activeVoteToMute.vote(source.getUniqueId(), true); + activeVoteToMute.vote(player.getUniqueId(), true); commandContext.getSource().sendMessage(Utility.parseMiniMessage( "You voted to mute. Thanks for voting, staff will be online soon to review!")); + player.getCurrentServer().ifPresent(serverConnection -> notifyEligiblePlayers(serverConnection.getServer(), activeVoteToMute)); } case "no" -> { - activeVoteToMute.vote(source.getUniqueId(), false); + activeVoteToMute.vote(player.getUniqueId(), false); commandContext.getSource().sendMessage(Utility.parseMiniMessage( "You voted not to mute. Thanks for voting, staff will be online soon to review!")); } @@ -249,6 +253,18 @@ public class VoteToMuteHelper { .count(); } + private void notifyEligiblePlayers(RegisteredServer server, ActiveVoteToMute activeVoteToMute) { + Component message = Utility.parseMiniMessage(" out of players have voted to mute ", + Placeholder.component("prefix", prefix), + Placeholder.parsed("voted_for", String.valueOf(activeVoteToMute.getVotedFor())), + Placeholder.parsed("total_votes", String.valueOf(activeVoteToMute.getTotalEligibleVoters())), + Placeholder.parsed("player", activeVoteToMute.getVotedPlayer().getUsername())); + boolean countLowerRanks = activeVoteToMute.countLowerRanks(); + server.getPlayersConnected().stream() + .filter(player -> countLowerRanks ? player.hasPermission("chat.backup-vote-to-mute") : player.hasPermission("chat.vote-to-mute")) + .forEach(player -> player.sendMessage(message)); + } + private void sendHelpMessage(CommandSource commandSource) { commandSource.sendMessage(Utility.parseMiniMessage("Use: /votetomutehelper .")); } diff --git a/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java index 9d629f1..65978cd 100644 --- a/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java +++ b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java @@ -28,6 +28,7 @@ public class ActiveVoteToMute { private static final Component prefix = Utility.parseMiniMessage("[VoteMute]"); private final Player votedPlayer; + private final Player startedByPlayer; private HashSet votedFor = new HashSet<>(); private HashSet votedAgainst = new HashSet<>(); private int totalEligibleVoters; @@ -74,7 +75,7 @@ public class ActiveVoteToMute { } public ActiveVoteToMute(@NotNull Player votedPlayer, @NotNull RegisteredServer server, ProxyServer proxyServer, Duration duration, - int totalEligibleVoters, boolean countLowerRanks, Component chatLogs) { + int totalEligibleVoters, boolean countLowerRanks, Component chatLogs, @NotNull Player startedByPlayer) { this.chatLogs = chatLogs; this.votedPlayer = votedPlayer; this.totalEligibleVoters = totalEligibleVoters; @@ -85,6 +86,7 @@ public class ActiveVoteToMute { ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); executorService.schedule(this::endVote, duration.toMinutes(), TimeUnit.MINUTES); + this.startedByPlayer = startedByPlayer; } private RegisteredServer getServer() { @@ -151,7 +153,7 @@ public class ActiveVoteToMute { .filter(player -> countLowerRanks ? player.hasPermission("chat.backup-vote-to-mute") : player.hasPermission("chat.vote-to-mute")) .forEach(player -> player.sendMessage(message)); proxyServer.getCommandManager().executeAsync(proxyServer.getConsoleCommandSource(), - String.format("tempmute %s 1h Muted by the community - under review.", votedPlayer.getUsername())); + String.format("tempmute %s 1h Muted by the community - under review. -p", votedPlayer.getUsername())); String chatLogsString = PlainTextComponentSerializer.plainText().serialize(chatLogs); @@ -179,7 +181,10 @@ public class ActiveVoteToMute { false); embedBuilder.addField("Server", server.getServerInfo().getName().substring(0, 1).toUpperCase() + server.getServerInfo().getName().substring(1), - false); + true); + embedBuilder.addField("Started by", + String.format("Username: %s\nUUID: %s", startedByPlayer.getUsername(), startedByPlayer.getUniqueId().toString()), + true); return embedBuilder; } @@ -218,4 +223,16 @@ public class ActiveVoteToMute { Placeholder.parsed("player", votedPlayer.getUsername()), Placeholder.component("logs", chatLogs)); } + + public Player getVotedPlayer() { + return votedPlayer; + } + + public int getVotedFor() { + return votedFor.size(); + } + + public int getTotalEligibleVoters() { + return totalEligibleVoters; + } } diff --git a/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/VoteToMuteStarter.java b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/VoteToMuteStarter.java index 66ea87e..dfafe3c 100644 --- a/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/VoteToMuteStarter.java +++ b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/VoteToMuteStarter.java @@ -42,7 +42,7 @@ public class VoteToMuteStarter { } public void start() { - chatLogHandler.retrieveChatLogs(votedPlayer.getUniqueId(), Duration.ofMinutes(5), serverName).whenCompleteAsync((chatLogs, throwable) -> { + chatLogHandler.retrieveChatLogs(votedPlayer.getUniqueId(), Duration.ofMinutes(10), serverName).whenCompleteAsync((chatLogs, throwable) -> { if (throwable != null) { commandSource.sendMessage(Utility.parseMiniMessage(" Unable to retrieve messages for player ", Placeholder.component("prefix", prefix), @@ -59,14 +59,15 @@ public class VoteToMuteStarter { private void parseChatLogs(List chatLogs) { TagResolver.Single playerTag = Placeholder.parsed("player", votedPlayer.getUsername()); + TagResolver.Single prefixTag = Placeholder.component("prefix", prefix); chatLogs.sort(Comparator.comparing(ChatLog::getTimestamp)); parsedChatLogs = IntStream.range(0, chatLogs.size()) .mapToObj(i -> Utility.parseMiniMessage( - ". [ChatLog] : ", + ". : ", TagResolver.resolver( Placeholder.unparsed("message", chatLogs.get(i).getMessage()), Placeholder.parsed("number", String.valueOf(i + 1)), - playerTag + playerTag, prefixTag )) ) .toList(); -- 2.39.5 From a908e32d0bc90dafbb5983209c4effa992614ee2 Mon Sep 17 00:00:00 2001 From: Teriuihi Date: Tue, 4 Jun 2024 21:18:57 +0200 Subject: [PATCH 15/15] Fix message validation and chat log order Corrected conditional logic in VoteToMuteHelper to validate messages properly and adjusted sorting of chat logs in VoteToMuteStarter to display in reverse chronological order. The update now accurately verifies the existence of selected messages and presents recent logs first for more user-friendly navigation. --- .../java/com/alttd/velocitychat/commands/VoteToMuteHelper.java | 2 +- .../velocitychat/commands/vote_to_mute/VoteToMuteStarter.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMuteHelper.java b/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMuteHelper.java index f646c81..4d87162 100644 --- a/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMuteHelper.java +++ b/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMuteHelper.java @@ -150,7 +150,7 @@ public class VoteToMuteHelper { } int highestLogEntry = max.get(); - if (voteToMuteStarter.getTotalLogEntries() > highestLogEntry) { + if (voteToMuteStarter.getTotalLogEntries() < highestLogEntry) { commandContext.getSource().sendMessage(Utility.parseMiniMessage("Some of your selected messages do not exist.")); return 1; } diff --git a/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/VoteToMuteStarter.java b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/VoteToMuteStarter.java index dfafe3c..57b213b 100644 --- a/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/VoteToMuteStarter.java +++ b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/VoteToMuteStarter.java @@ -60,7 +60,7 @@ public class VoteToMuteStarter { private void parseChatLogs(List chatLogs) { TagResolver.Single playerTag = Placeholder.parsed("player", votedPlayer.getUsername()); TagResolver.Single prefixTag = Placeholder.component("prefix", prefix); - chatLogs.sort(Comparator.comparing(ChatLog::getTimestamp)); + chatLogs.sort(Comparator.comparing(ChatLog::getTimestamp).reversed()); parsedChatLogs = IntStream.range(0, chatLogs.size()) .mapToObj(i -> Utility.parseMiniMessage( ". : ", -- 2.39.5