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); + } +}