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 7d8dfaf..0e7e8cb 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..1554450 --- /dev/null +++ b/api/src/main/java/com/alttd/chat/database/ChatLogQueries.java @@ -0,0 +1,98 @@ +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 org.jetbrains.annotations.NotNull; + +import java.sql.*; +import java.time.Duration; +import java.time.Instant; +import java.util.*; +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(HashMap> 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..a3fee53 --- /dev/null +++ b/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLogHandler.java @@ -0,0 +1,128 @@ +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 org.jetbrains.annotations.NotNull; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.Duration; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.*; + +public class ChatLogHandler { + + private static ChatLogHandler instance = null; + private ScheduledExecutorService executorService = null; + + public static ChatLogHandler getInstance(boolean enableLogging) { + if (instance == null) + instance = new ChatLogHandler(enableLogging); + return instance; + } + + private boolean isSaving; + private final Queue chatLogQueue = new ConcurrentLinkedQueue<>(); + private final HashMap> chatLogs = new HashMap<>(); + + public ChatLogHandler(boolean 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) { + 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); + 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 has started!"); + } + + /** + * 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); + } + + 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); + 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) -> { + if (throwable == null && result) { + chatLogs.clear(); + } else { + 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"); + }); + } + + 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..9721038 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(true); + 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/ChatListener.java b/galaxy/src/main/java/com/alttd/chat/listeners/ChatListener.java index 29aece6..0e40256 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) @@ -97,6 +103,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/PlayerListener.java b/galaxy/src/main/java/com/alttd/chat/listeners/PlayerListener.java index 060135e..e748e0e 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; @@ -111,15 +112,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); } } 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..6b4dfc0 --- /dev/null +++ b/galaxy/src/main/java/com/alttd/chat/listeners/ShutdownListener.java @@ -0,0 +1,27 @@ +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; +import org.bukkit.plugin.Plugin; + +public class ShutdownListener implements Listener { + + private final ChatLogHandler chatLogHandler; + private final Plugin thisPlugin; + + 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/VelocityChat.java b/velocity/src/main/java/com/alttd/velocitychat/VelocityChat.java index 6eeac13..d7cb3a9 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,11 +110,14 @@ public class VelocityChat { } public void loadCommands() { + ChatLogHandler instance = ChatLogHandler.getInstance(false); new SilentJoinCommand(server); new GlobalAdminChat(server); new Reload(server); new MailCommand(server); new Report(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 new file mode 100644 index 0000000..7a7e260 --- /dev/null +++ b/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMute.java @@ -0,0 +1,163 @@ +package com.alttd.velocitychat.commands; + +import com.alttd.chat.objects.chat_log.ChatLogHandler; +import com.alttd.chat.util.Utility; +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; +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.Collection; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +public class VoteToMute { + + public VoteToMute(ProxyServer proxyServer, ChatLogHandler chatLogHandler) { + 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; + }); + + 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.", + 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(); + 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 < 6) { + countLowerRanks = true; + count = getTotalEligiblePlayers(server, true); + if (count < 6) { + 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; + })) + .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: /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/VoteToMuteHelper.java b/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMuteHelper.java new file mode 100644 index 0000000..4d87162 --- /dev/null +++ b/velocity/src/main/java/com/alttd/velocitychat/commands/VoteToMuteHelper.java @@ -0,0 +1,285 @@ +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 jdk.jshell.execution.Util; +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 { + + private static final Component prefix = Utility.parseMiniMessage("[VoteMute]"); + + 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 enterMessagesNode = 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, player) + .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 player)) { + commandContext.getSource().sendMessage(Utility.parseMiniMessage( + "Only players are allowed to vote")); + return 1; + } + 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 (!player.hasPermission("chat.vote-to-mute")) { + player.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(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(player.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))); + } + 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(enterMessagesNode) + .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 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 .")); + } + + 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..65978cd --- /dev/null +++ b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/ActiveVoteToMute.java @@ -0,0 +1,238 @@ +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; +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.*; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +public class ActiveVoteToMute { + + private static final HashMap instances = new HashMap<>(); + 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; + private final boolean countLowerRanks; + 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)) + return Optional.empty(); + return Optional.of(instances.get(username)); + } + + 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 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, @NotNull Player startedByPlayer) { + 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); + this.startedByPlayer = startedByPlayer; + } + + private RegisteredServer getServer() { + return server; + } + + private void endVote() { + if (endedVote) + return; + 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)); + } + + public void vote(UUID uuid, boolean votedToMute) { + if (votedToMute) { + votedFor.add(uuid); + votedAgainst.remove(uuid); + if (!votePassed()) { + return; + } + endedVote = true; + instances.remove(votedPlayer.getUsername()); + mutePlayer(); + } else { + votedAgainst.add(uuid); + votedFor.remove(uuid); + } + } + + public boolean votePassed() { + double totalVotes = (votedFor.size() + votedAgainst.size()); + if (totalVotes == 0 || votedFor.isEmpty()) { + return false; + } + if (totalVotes / totalEligibleVoters < 0.6) { + return false; + } + return votedFor.size() / totalVotes > 0.6; + } + + 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. -p", 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); + 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), + true); + embedBuilder.addField("Started by", + String.format("Username: %s\nUUID: %s", startedByPlayer.getUsername(), startedByPlayer.getUniqueId().toString()), + true); + return embedBuilder; + } + + 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) { + 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(""" + 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)); + } + + 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 new file mode 100644 index 0000000..57b213b --- /dev/null +++ b/velocity/src/main/java/com/alttd/velocitychat/commands/vote_to_mute/VoteToMuteStarter.java @@ -0,0 +1,129 @@ +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; + private final boolean countLowerRanks; + + 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, boolean countLowerRanks) { + this.chatLogHandler = chatLogHandler; + this.votedPlayer = votedPlayer; + this.commandSource = commandSource; + this.serverName = serverName; + this.countLowerRanks = countLowerRanks; + instanceMap.put(commandSource.getUniqueId(), this); + } + + public void start() { + 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), + 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: /votetomutehelper messages 1, 2, 5, 8", Placeholder.component("prefix", prefix))); + showPage(1); + }); + } + + 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).reversed()); + parsedChatLogs = IntStream.range(0, chatLogs.size()) + .mapToObj(i -> Utility.parseMiniMessage( + ". : ", + TagResolver.resolver( + Placeholder.unparsed("message", chatLogs.get(i).getMessage()), + Placeholder.parsed("number", String.valueOf(i + 1)), + playerTag, prefixTag + )) + ) + .toList(); + } + + public void showPage(int page) { + 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 > 1) { + 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); + } + + 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;