From 44d6e994ccdfc44c508a991efaf9fdcaba32bc22 Mon Sep 17 00:00:00 2001 From: Teriuihi Date: Sat, 6 Apr 2024 17:25:15 +0200 Subject: [PATCH 1/8] 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(); + } + +} From ed2ba7477280773b0c19ed38bb2785b755ee0e46 Mon Sep 17 00:00:00 2001 From: Teriuihi Date: Sat, 6 Apr 2024 19:39:36 +0200 Subject: [PATCH 2/8] 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); + } +} From 37aa9fdf4c11b43a8ca8bbebfeaf17ebb78f299e Mon Sep 17 00:00:00 2001 From: Teriuihi Date: Sun, 7 Apr 2024 16:32:33 +0200 Subject: [PATCH 3/8] 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); From c25767e473390372213f29debfe042ac1cc65ed9 Mon Sep 17 00:00:00 2001 From: Teriuihi Date: Sun, 7 Apr 2024 18:07:20 +0200 Subject: [PATCH 4/8] 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; From c057c6965314e7c473c98f95fd7baa6e475d87e0 Mon Sep 17 00:00:00 2001 From: Teriuihi Date: Sun, 7 Apr 2024 18:11:19 +0200 Subject: [PATCH 5/8] 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); From d6135f2456e1d82b0f5d76d84e53c8486caa2827 Mon Sep 17 00:00:00 2001 From: Teriuihi Date: Sun, 7 Apr 2024 18:29:46 +0200 Subject: [PATCH 6/8] 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) { From 192fca3a89ab24d6026db47477ee0e27832c39e6 Mon Sep 17 00:00:00 2001 From: Teriuihi Date: Sun, 7 Apr 2024 19:16:32 +0200 Subject: [PATCH 7/8] 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()) { From 3b2aa84164a2805af7b9e16a9f8cb9b3fca28c79 Mon Sep 17 00:00:00 2001 From: Teriuihi Date: Sat, 13 Apr 2024 16:52:56 +0200 Subject: [PATCH 8/8] 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) {