diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/chatMention/ChatMentionListener.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/chatMention/ChatMentionListener.java index 7f9a0ba..13a32c9 100644 --- a/src/main/java/eu/mhsl/craftattack/spawn/appliances/chatMention/ChatMentionListener.java +++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/chatMention/ChatMentionListener.java @@ -1,6 +1,8 @@ package eu.mhsl.craftattack.spawn.appliances.chatMention; +import eu.mhsl.craftattack.spawn.Main; import eu.mhsl.craftattack.spawn.appliance.ApplianceListener; +import eu.mhsl.craftattack.spawn.appliances.chatMessages.ChatMessages; import eu.mhsl.craftattack.spawn.appliances.settings.Settings; import eu.mhsl.craftattack.spawn.util.text.ComponentUtil; import io.papermc.paper.event.player.AsyncChatDecorateEvent; @@ -8,6 +10,7 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.bukkit.event.EventHandler; +import org.bukkit.event.player.PlayerJoinEvent; import java.util.ArrayList; import java.util.List; @@ -22,6 +25,7 @@ public class ChatMentionListener extends ApplianceListener { ChatMentionSetting.ChatMentionConfig config = Settings.instance() .getSetting(event.player(), Settings.Key.ChatMentions, ChatMentionSetting.ChatMentionConfig.class); + ChatMessages chatMessages = Main.instance().getAppliance(ChatMessages.class); Component result = words.stream() .map(word -> { @@ -29,10 +33,11 @@ public class ChatMentionListener extends ApplianceListener { boolean isPlayer = getAppliance().getPlayerNames().contains(wordWithoutAnnotation); if(isPlayer && config.applyMentions()) { mentioned.add(wordWithoutAnnotation); - return Component.text( + Component mention = Component.text( getAppliance().formatPlayer(wordWithoutAnnotation), NamedTextColor.GOLD ); + return chatMessages.addReportActions(mention, wordWithoutAnnotation); } else { return Component.text(word); } @@ -43,4 +48,9 @@ public class ChatMentionListener extends ApplianceListener { getAppliance().notifyPlayers(mentioned); event.result(result); } + + @EventHandler + public void onJoin(PlayerJoinEvent event) { + getAppliance().refreshPlayers(); + } } diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/chatMessages/ChatMessages.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/chatMessages/ChatMessages.java index 337ab32..3f48ad3 100644 --- a/src/main/java/eu/mhsl/craftattack/spawn/appliances/chatMessages/ChatMessages.java +++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/chatMessages/ChatMessages.java @@ -19,11 +19,13 @@ public class ChatMessages extends Appliance { } public Component getReportablePlayerName(Player player) { - return Component - .text("") - .append(player.displayName()) + return addReportActions(player.displayName(), player.getName()); + } + + public Component addReportActions(Component message, String username) { + return message .hoverEvent(HoverEvent.showText(Component.text("Klicke, um diesen Spieler zu reporten").color(NamedTextColor.GOLD))) - .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/report " + player.getName() + " ")); + .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND, String.format("/report %s ", username))); } @Override diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/privateMessage/PrivateMessage.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/privateMessage/PrivateMessage.java new file mode 100644 index 0000000..6ab32d1 --- /dev/null +++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/privateMessage/PrivateMessage.java @@ -0,0 +1,187 @@ +package eu.mhsl.craftattack.spawn.appliances.privateMessage; + +import eu.mhsl.craftattack.spawn.Main; +import eu.mhsl.craftattack.spawn.appliance.Appliance; +import eu.mhsl.craftattack.spawn.appliance.ApplianceCommand; +import eu.mhsl.craftattack.spawn.appliances.chatMessages.ChatMessages; +import eu.mhsl.craftattack.spawn.appliances.privateMessage.commands.PrivateMessageCommand; +import eu.mhsl.craftattack.spawn.appliances.privateMessage.commands.PrivateReplyCommand; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.ComponentBuilder; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.stream.Collectors; + +public class PrivateMessage extends Appliance { + public final int targetChangeTimeoutSeconds = 30; + public final int conversationTimeoutMinutes = 30; + + private record Conversation(UUID target, Long lastSet) {} + private final Map> replyMapping = new WeakHashMap<>(); + + public void reply(Player sender, String message) { + this.replyMapping.computeIfAbsent(sender, player -> new ArrayList<>()); + + List replyList = this.replyMapping.get(sender); + + List tooOldConversations = replyList.stream() + .filter(conversation -> conversation.lastSet < System.currentTimeMillis() - (conversationTimeoutMinutes*60*1000)) + .toList(); + replyList.removeAll(tooOldConversations); + + if(replyList.isEmpty()) throw new ApplianceCommand.Error("Du führst aktuell keine Konversation."); + + Component privatePrefix = Component.text("[Privat] ", NamedTextColor.LIGHT_PURPLE); + + Conversation youngestEntry = replyList.stream() + .max(Comparator.comparingLong(o -> o.lastSet)) + .orElse(replyList.getLast()); + + if(message.isBlank()) { + Player currentTargetPlayer = Bukkit.getPlayer(youngestEntry.target()); + + Component currentTargetComponent = currentTargetPlayer != null + ? Main.instance().getAppliance(ChatMessages.class).getReportablePlayerName(currentTargetPlayer) + : Component.text("niemandem."); + + sender.sendMessage( + privatePrefix + .append(Component.text("Du schreibst aktuell mit ", NamedTextColor.GRAY)) + .append(currentTargetComponent) + ); + return; + } + + List oldConversations = replyList.stream() + .filter(conversation -> conversation.lastSet < System.currentTimeMillis() - (targetChangeTimeoutSeconds*1000)) + .toList(); + + if(oldConversations.contains(youngestEntry) || replyList.size() == 1) { + Player target = Bukkit.getPlayer(youngestEntry.target()); + if(target == null) throw new ApplianceCommand.Error("Der Spieler " + Bukkit.getOfflinePlayer(youngestEntry.target()).getName() + " ist nicht mehr verfügbar."); + + replyList.clear(); + this.sendWhisper(sender, new ResolvedPmUserArguments(target, message)); + return; + } + + ComponentBuilder component = Component.text(); + + component.append( + Component.newline() + .append(privatePrefix) + .append(Component.text("Das Ziel für /r hat sich bei dir in den letzten ", NamedTextColor.RED)) + .append(Component.text(String.valueOf(this.targetChangeTimeoutSeconds), NamedTextColor.RED)) + .append(Component.text(" Sekunden geändert. Wer soll deine Nachricht erhalten? ", NamedTextColor.RED)) + .appendNewline() + .appendNewline() + ); + + if(!oldConversations.isEmpty()) { + Conversation youngestOldConversation = oldConversations.stream() + .max(Comparator.comparingLong(o -> o.lastSet)) + .orElse(oldConversations.getLast()); + replyList.removeAll(oldConversations); + replyList.add(youngestOldConversation); + } + + List playerNames = replyList.stream() + .map(conversation -> Bukkit.getOfflinePlayer(conversation.target()).getName()) + .distinct() + .toList(); + + playerNames.forEach(playerName -> component.append( + Component.text("[") + .append(Component.text(playerName, NamedTextColor.GOLD)) + .append(Component.text("]")) + .clickEvent(ClickEvent.runCommand(String.format("/msg %s %s", playerName, message))) + .hoverEvent(HoverEvent.showText(Component.text("Klicke, um diesem Spieler zu schreiben.").color(NamedTextColor.GOLD)))) + .append(Component.text(" ")) + ); + component.appendNewline(); + + sender.sendMessage(component.build()); + + } + + public void sendWhisper(Player sender, ResolvedPmUserArguments userArguments) { + Conversation newReceiverConversation = new Conversation( + sender.getUniqueId(), + System.currentTimeMillis() + ); + + if(this.replyMapping.get(userArguments.receiver) != null) { + List oldEntries = this.replyMapping.get(userArguments.receiver).stream() + .filter(conversation -> conversation.target() == sender.getUniqueId()) + .toList(); + this.replyMapping.get(userArguments.receiver).removeAll(oldEntries); + } else { + this.replyMapping.put( + userArguments.receiver, + new ArrayList<>() + ); + } + + this.replyMapping.get(userArguments.receiver).add(newReceiverConversation); + + List senderConversationList = new ArrayList<>(); + senderConversationList.add( + new Conversation( + userArguments.receiver.getUniqueId(), + System.currentTimeMillis() + ) + ); + + this.replyMapping.put( + sender, + senderConversationList + ); + + ChatMessages chatMessages = Main.instance().getAppliance(ChatMessages.class); + Component privatePrefix = Component.text("[Privat] ", NamedTextColor.LIGHT_PURPLE); + + sender.sendMessage( + Component.text() + .append(privatePrefix.clickEvent(ClickEvent.suggestCommand(String.format("/msg %s ", userArguments.receiver.getName())))) + .append(sender.displayName()) + .append(Component.text(" zu ", NamedTextColor.GRAY)) + .append(chatMessages.getReportablePlayerName(userArguments.receiver)) + .append(Component.text(" > ", NamedTextColor.GRAY)) + .append(Component.text(userArguments.message)) + ); + userArguments.receiver.sendMessage( + Component.text() + .append(privatePrefix.clickEvent(ClickEvent.suggestCommand(String.format("/msg %s ", sender.getName())))) + .append(chatMessages.getReportablePlayerName(sender)) + .append(Component.text(" zu ", NamedTextColor.GRAY)) + .append(userArguments.receiver.displayName()) + .append(Component.text(" > ", NamedTextColor.GRAY)) + .append(Component.text(userArguments.message)) + ); + } + + public record ResolvedPmUserArguments(Player receiver, String message) {} + public ResolvedPmUserArguments resolveImplicit(String[] args) { + if(args.length < 2) throw new ApplianceCommand.Error("Es muss ein Spieler sowie eine Nachricht angegeben werden."); + List arguments = List.of(args); + Player targetPlayer = Bukkit.getPlayer(arguments.getFirst()); + if(targetPlayer == null) throw new ApplianceCommand.Error(String.format("Der Spieler %s konnte nicht gefunden werden.", arguments.getFirst())); + String message = arguments.stream().skip(1).collect(Collectors.joining(" ")); + return new ResolvedPmUserArguments(targetPlayer, message); + } + + @Override + protected @NotNull List> commands() { + return List.of( + new PrivateMessageCommand(), + new PrivateReplyCommand() + ); + } +} diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/privateMessage/commands/PrivateMessageCommand.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/privateMessage/commands/PrivateMessageCommand.java new file mode 100644 index 0000000..711bbe3 --- /dev/null +++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/privateMessage/commands/PrivateMessageCommand.java @@ -0,0 +1,18 @@ +package eu.mhsl.craftattack.spawn.appliances.privateMessage.commands; + +import eu.mhsl.craftattack.spawn.appliance.ApplianceCommand; +import eu.mhsl.craftattack.spawn.appliances.privateMessage.PrivateMessage; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +public class PrivateMessageCommand extends ApplianceCommand.PlayerChecked { + public PrivateMessageCommand() { + super("msg"); + } + + @Override + protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception { + getAppliance().sendWhisper(getPlayer(), getAppliance().resolveImplicit(args)); + } +} diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/privateMessage/commands/PrivateReplyCommand.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/privateMessage/commands/PrivateReplyCommand.java new file mode 100644 index 0000000..fd7de78 --- /dev/null +++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/privateMessage/commands/PrivateReplyCommand.java @@ -0,0 +1,18 @@ +package eu.mhsl.craftattack.spawn.appliances.privateMessage.commands; + +import eu.mhsl.craftattack.spawn.appliance.ApplianceCommand; +import eu.mhsl.craftattack.spawn.appliances.privateMessage.PrivateMessage; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +public class PrivateReplyCommand extends ApplianceCommand.PlayerChecked { + public PrivateReplyCommand() { + super("r"); + } + + @Override + protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception { + getAppliance().reply(getPlayer(), String.join(" ", args)); + } +} diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/whitelist/Whitelist.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/whitelist/Whitelist.java index 16da0c5..dd707fb 100644 --- a/src/main/java/eu/mhsl/craftattack/spawn/appliances/whitelist/Whitelist.java +++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/whitelist/Whitelist.java @@ -66,12 +66,9 @@ public class Whitelist extends Appliance { queryAppliance(Outlawed.class).updateForcedStatus(player, timestampRelevant(user.outlawed_until)); - String purePlayerName; - if(Floodgate.isBedrock(player)) { - purePlayerName = Floodgate.getBedrockPlayer(player).getUsername(); - } else { - purePlayerName = player.getName(); - } + String purePlayerName = Floodgate.isBedrock(player) + ? Floodgate.getBedrockPlayer(player).getUsername() + : player.getName(); if(!user.username.trim().equalsIgnoreCase(purePlayerName)) throw new DisconnectInfo.Throwable( diff --git a/src/main/java/eu/mhsl/craftattack/spawn/util/statistics/NetworkMonitor.java b/src/main/java/eu/mhsl/craftattack/spawn/util/statistics/NetworkMonitor.java index c9ea77c..88719e2 100644 --- a/src/main/java/eu/mhsl/craftattack/spawn/util/statistics/NetworkMonitor.java +++ b/src/main/java/eu/mhsl/craftattack/spawn/util/statistics/NetworkMonitor.java @@ -9,6 +9,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.time.Duration; +import java.util.logging.Level; public class NetworkMonitor { private long previousRxBytes = 0; @@ -78,8 +79,9 @@ public class NetworkMonitor { String content = new String(Files.readAllBytes(Paths.get(path))); return Long.parseLong(content.trim()); } catch(IOException e) { + Main.logger().log(Level.SEVERE, "Statistics are only supported on Linux! Is tablist.interface config set correctly?"); this.stop(); - throw new RuntimeException("Failed receiving Network statistic, disabling statistics!", e); + throw new RuntimeException("Failed reading network statistic", e); } } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 11d8e20..a6240d1 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -49,6 +49,7 @@ playerLimit: maxPlayers: 10 whitelist: + overrideIntegrityCheck: false api: https://mhsl.eu/craftattack/api/user tablist: diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 84efe67..0a3437b 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -39,4 +39,6 @@ commands: settings: texturepack: maintanance: - yearRank: \ No newline at end of file + yearRank: + msg: + r: \ No newline at end of file