From 6db9816c8543e8c2b7f946a7afe917a1b3e825e0 Mon Sep 17 00:00:00 2001 From: akastijn Date: Sun, 2 Aug 2026 15:46:51 +0200 Subject: [PATCH] Refactor chat message handling. It now supports sending chat messages with just a UUID and a string message. (For support for web messages) --- .../main/java/com/alttd/chat/ChatPlugin.java | 4 +- .../chat/chat_web/ChatMessageSender.java | 278 ++++++++++++++++++ .../alttd/chat/listeners/ChatListener.java | 178 +---------- .../com/alttd/chat/util/GalaxyUtility.java | 19 +- 4 files changed, 302 insertions(+), 177 deletions(-) create mode 100644 galaxy/src/main/java/com/alttd/chat/chat_web/ChatMessageSender.java diff --git a/galaxy/src/main/java/com/alttd/chat/ChatPlugin.java b/galaxy/src/main/java/com/alttd/chat/ChatPlugin.java index 4ef5f6e..6b42292 100755 --- a/galaxy/src/main/java/com/alttd/chat/ChatPlugin.java +++ b/galaxy/src/main/java/com/alttd/chat/ChatPlugin.java @@ -1,5 +1,6 @@ package com.alttd.chat; +import com.alttd.chat.chat_web.ChatMessageSender; import com.alttd.chat.commands.*; import com.alttd.chat.config.Config; import com.alttd.chat.config.ServerConfig; @@ -39,8 +40,9 @@ public class ChatPlugin extends JavaPlugin { chatHandler = new ChatHandler(chatLogHandler); DatabaseConnection.initialize(); serverConfig = new ServerConfig(ServerName.getServerName()); + ChatMessageSender chatMessageSender = new ChatMessageSender(chatLogHandler, chatAPI.getLuckPerms()); registerListener(new PlayerListener(serverConfig), - new ChatListener(chatLogHandler), + new ChatListener(chatMessageSender), new BookListener(), new ShutdownListener(chatLogHandler, this) ); diff --git a/galaxy/src/main/java/com/alttd/chat/chat_web/ChatMessageSender.java b/galaxy/src/main/java/com/alttd/chat/chat_web/ChatMessageSender.java new file mode 100644 index 0000000..9e2308e --- /dev/null +++ b/galaxy/src/main/java/com/alttd/chat/chat_web/ChatMessageSender.java @@ -0,0 +1,278 @@ +package com.alttd.chat.chat_web; + +import com.alttd.chat.ChatPlugin; +import com.alttd.chat.config.Config; +import com.alttd.chat.managers.ChatUserManager; +import com.alttd.chat.managers.RegexManager; +import com.alttd.chat.objects.ChatUser; +import com.alttd.chat.objects.FilterType; +import com.alttd.chat.objects.ModifiableString; +import com.alttd.chat.objects.chat_log.ChatLogHandler; +import com.alttd.chat.objects.chat_log.ChatLogType; +import com.alttd.chat.util.ALogger; +import com.alttd.chat.util.GalaxyUtility; +import com.alttd.chat.util.ServerName; +import com.alttd.chat.util.Utility; +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; +import lombok.RequiredArgsConstructor; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.ComponentLike; +import net.kyori.adventure.text.TextReplacementConfig; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import net.luckperms.api.LuckPerms; +import net.luckperms.api.context.ImmutableContextSet; +import net.luckperms.api.model.user.User; +import net.luckperms.api.query.QueryOptions; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@RequiredArgsConstructor +public class ChatMessageSender { + + private final ChatLogHandler chatLogHandler; + private final LuckPerms luckPerms; + private final MiniMessage miniMessage = MiniMessage.miniMessage(); + + public void sendMessage(UUID sender, String message) { + OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(sender); + Component component = miniMessage.deserialize(message); + sendMessage(offlinePlayer, component); + } + + public void sendMessage(@NotNull OfflinePlayer offlinePlayer, Component message) { + UUID uuid = offlinePlayer.getUniqueId(); + if (luckPerms.getUserManager().isLoaded(uuid)) { + User user = luckPerms.getUserManager().getUser(uuid); + sendMessage(user, offlinePlayer, message); + } else { + luckPerms.getUserManager().loadUser(uuid).whenComplete((user, throwable) -> { + if (throwable != null) { + ALogger.error("Failed to load user: " + uuid, throwable); + } + sendMessage(user, offlinePlayer, message); + }); + } + } + + private void sendMessage(User user, OfflinePlayer offlinePlayer, Component message) { + if (offlinePlayer == null) { + ALogger.error("OfflinePlayer is null"); + return; + } + if (ChatPlugin.getInstance().serverMuted() && !hasPermission(user, "chat.bypass-server-muted")) { + sendBlockNotifIfOnline(offlinePlayer, message); + return; + } + + UUID uuid = offlinePlayer.getUniqueId(); + + ComponentLike input = message.colorIfAbsent(NamedTextColor.WHITE); + Component inputComponent = input.asComponent(); + + ModifiableString modifiableString = new ModifiableString(inputComponent); + + if (parseMessage(offlinePlayer, uuid, modifiableString, inputComponent)) { + //Parse message failed likely due to something the player said, already logged + return; + } + + if (sendMessageAndPing(user, offlinePlayer, uuid, modifiableString)) { + //Send message and ping failed, likely due to the offlinePlayer not being valid, already logged + return; + } + ALogger.info(PlainTextComponentSerializer.plainText().serialize(inputComponent)); + } + + private boolean sendMessageAndPing(User user, OfflinePlayer offlinePlayer, UUID uuid, ModifiableString modifiableString) { + ComponentLike input; + Stream stream = Bukkit.getOnlinePlayers().stream() + .map(audience -> (Player) audience); + + if (!hasPermission(user, "chat.ignorebypass")) { + stream = stream.filter(receiver -> { + boolean isPlayerIgnored = ChatUserManager + .getChatUser(receiver.getUniqueId()) + .getIgnoredPlayers() + .contains(uuid); + return !isPlayerIgnored || receiver.hasPermission("chat.ignorebypass"); + }); + } + Set receivers = stream.collect(Collectors.toSet()); + + Set playersToPing = new HashSet<>(); + pingPlayers(playersToPing, modifiableString, offlinePlayer, user); + + Optional render = render(offlinePlayer, modifiableString.component()); + if (render.isEmpty()) { + //Already logged + return true; + } + input = render.get(); + for (Player receiver : receivers) { + receiver.sendMessage(input); + } + for (Player pingPlayer : playersToPing) { + pingPlayer.playSound(pingPlayer.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1, 1); + } + chatLogHandler.addChatLog(uuid, + ServerName.getServerName(), + modifiableString.string(), + ChatLogType.PUBLIC, + null, + null, + input.asComponent(), + false + ); + return false; + } + + private boolean parseMessage(OfflinePlayer offlinePlayer, UUID uuid, ModifiableString modifiableString, Component inputComponent) { + // todo a better way for this + if (!RegexManager.filterText(offlinePlayer.getName(), uuid, modifiableString, true, "chat", + filterType -> punishOnlinePlayer(filterType, uuid, modifiableString) + )) { + GalaxyUtility.sendBlockedNotification("Language", offlinePlayer, + modifiableString.component(), + "" + ); + String originalMessage = PlainTextComponentSerializer.plainText().serialize(inputComponent); + Optional component = render(offlinePlayer, inputComponent).map(ComponentLike::asComponent); + if (component.isEmpty()) { + //Already logged + return true; + } + chatLogHandler.addChatLog(uuid, + ServerName.getServerName(), + originalMessage, + ChatLogType.PUBLIC, + null, + null, + component.get(), + true + ); + return true; + } + return false; + } + + private void pingPlayers(Set playersToPing, ModifiableString modifiableString, OfflinePlayer offlinePlayer, User user) { + Component mention = MiniMessage.miniMessage().deserialize(Config.MENTIONPLAYERTAG); + for (Player onlinePlayer : Bukkit.getOnlinePlayers()) { + String name = onlinePlayer.getName(); + String nickName = PlainTextComponentSerializer.plainText().serialize(onlinePlayer.displayName()); + + Pattern namePattern = Pattern.compile("\\b(? { + // String substring = a.group().substring(1); + // return ; + // }); + + if (!ChatUserManager.getChatUser(onlinePlayer.getUniqueId()) + .getIgnoredPlayers() + .contains(offlinePlayer.getUniqueId()) + || hasPermission(user, "chat.ignorebypass")) { + playersToPing.add(onlinePlayer); + } + } else if (nickPattern.matcher(modifiableString.string()).find()) { + modifiableString.replace(TextReplacementConfig.builder() + .once() + .match(nickPattern) + .replacement(mention.append(onlinePlayerUser.getDisplayName())) + .build()); + if (!ChatUserManager.getChatUser(onlinePlayer.getUniqueId()) + .getIgnoredPlayers() + .contains(offlinePlayer.getUniqueId()) + || hasPermission(user, "chat.ignorebypass")) { + playersToPing.add(onlinePlayer); + } + } + } + } + + private Optional render(@NotNull OfflinePlayer offlinePlayer, @NotNull Component message) { + if (offlinePlayer.getName() == null) { + ALogger.error("Invalid offline player"); + return Optional.empty(); + } + + ChatUser user = ChatUserManager.getChatUser(offlinePlayer.getUniqueId()); + TagResolver placeholders = TagResolver.resolver( + Placeholder.component("sender", user.getDisplayName()), + Placeholder.parsed("sendername", offlinePlayer.getName()), + Placeholder.component("prefix", user.getPrefix()), + Placeholder.component("prefixall", user.getPrefixAll()), + Placeholder.component("staffprefix", user.getStaffPrefix()), + Placeholder.component("message", message) + ); + + return Optional.of(Utility.parseMiniMessage(Config.CHATFORMAT, placeholders)); + } + + private static void punishOnlinePlayer(FilterType filterType, UUID uuid, ModifiableString modifiableString) { + Player player = Bukkit.getPlayer(uuid); + if (player == null) { + return; + } + if (!filterType.equals(FilterType.PUNISH)) { + ALogger.warn("Received another FilterType than punish when filtering chat and executing a filter action"); + return; + } + ByteArrayDataOutput out = ByteStreams.newDataOutput(); + out.writeUTF("punish"); + out.writeUTF(player.getName()); + out.writeUTF(uuid.toString()); + out.writeUTF(modifiableString.string()); + player.sendPluginMessage(ChatPlugin.getInstance(), Config.MESSAGECHANNEL, out.toByteArray()); + } + + private static void sendBlockNotifIfOnline(OfflinePlayer offlinePlayer, Component message) { + if (!offlinePlayer.isOnline()) { + return; + } + Player player = offlinePlayer.getPlayer(); + if (player == null) { + ALogger.error("Failed to load player: " + offlinePlayer.getName()); + return; + } + GalaxyUtility.sendBlockedNotification("Chat Muted", player, message, ""); + } + + private boolean hasPermission(User user, String permission) { + return user.getCachedData() + .getPermissionData(QueryOptions.contextual(ImmutableContextSet.of("server", luckPerms.getServerName()))) + .checkPermission(permission) + .asBoolean(); + } + +} 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 7eac7bc..09ba4b2 100755 --- a/galaxy/src/main/java/com/alttd/chat/listeners/ChatListener.java +++ b/galaxy/src/main/java/com/alttd/chat/listeners/ChatListener.java @@ -1,53 +1,32 @@ package com.alttd.chat.listeners; -import com.alttd.chat.ChatPlugin; +import com.alttd.chat.chat_web.ChatMessageSender; import com.alttd.chat.config.Config; 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.objects.chat_log.ChatLogType; -import com.alttd.chat.util.ALogger; -import com.alttd.chat.util.GalaxyUtility; -import com.alttd.chat.util.ServerName; +import com.alttd.chat.objects.ChatFilter; +import com.alttd.chat.objects.Toggleable; import com.alttd.chat.util.Utility; -import com.google.common.io.ByteArrayDataOutput; -import com.google.common.io.ByteStreams; import io.papermc.paper.event.player.AsyncChatCommandDecorateEvent; import io.papermc.paper.event.player.AsyncChatDecorateEvent; import io.papermc.paper.event.player.AsyncChatEvent; +import lombok.RequiredArgsConstructor; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.ComponentLike; import net.kyori.adventure.text.TextReplacementConfig; -import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.minimessage.MiniMessage; -import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; -import org.bukkit.Bukkit; -import org.bukkit.Sound; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; -import org.jetbrains.annotations.NotNull; - -import java.util.HashSet; -import java.util.Set; -import java.util.UUID; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import java.util.stream.Stream; +@RequiredArgsConstructor public class ChatListener implements Listener { private final PlainTextComponentSerializer plainTextComponentSerializer = PlainTextComponentSerializer.plainText(); - private final ChatLogHandler chatLogHandler; - - public ChatListener(ChatLogHandler chatLogHandler) { - this.chatLogHandler = chatLogHandler; - } + private final ChatMessageSender chatMessageSender; @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onChatCommandDecorate(AsyncChatCommandDecorateEvent event) { @@ -83,8 +62,6 @@ public class ChatListener implements Listener { .build())); } - private final Component mention = MiniMessage.miniMessage().deserialize(Config.MENTIONPLAYERTAG); - @EventHandler(ignoreCancelled = true) public void onPlayerChat(AsyncChatEvent event) { event.setCancelled(true); //Always cancel the event because we do not want to deal with Microsoft's stupid bans @@ -93,148 +70,7 @@ public class ChatListener implements Listener { toggleable.sendMessage(event.getPlayer(), event.message()); return; } - if (ChatPlugin.getInstance().serverMuted() && !event.getPlayer().hasPermission("chat.bypass-server-muted")) { - Player player = event.getPlayer(); - GalaxyUtility.sendBlockedNotification("Chat Muted", player, event.message(), ""); - return; - } - - Player player = event.getPlayer(); - UUID uuid = player.getUniqueId(); - - ComponentLike input = event.message().colorIfAbsent(NamedTextColor.WHITE); - Component inputComponent = input.asComponent(); - - ModifiableString modifiableString = new ModifiableString(inputComponent); - - // todo a better way for this - if (!RegexManager.filterText(player.getName(), uuid, modifiableString, true, "chat", filterType -> { - if (!filterType.equals(FilterType.PUNISH)) { - ALogger.warn("Received another FilterType than punish when filtering chat and executing a filter action"); - return; - } - ByteArrayDataOutput out = ByteStreams.newDataOutput(); - out.writeUTF("punish"); - out.writeUTF(player.getName()); - out.writeUTF(uuid.toString()); - out.writeUTF(modifiableString.string()); - player.sendPluginMessage(ChatPlugin.getInstance(), Config.MESSAGECHANNEL, out.toByteArray()); - } - )) { - event.setCancelled(true); - GalaxyUtility.sendBlockedNotification("Language", player, - modifiableString.component(), - "" - ); - String originalMessage = PlainTextComponentSerializer.plainText().serialize(inputComponent); - chatLogHandler.addChatLog(uuid, - ServerName.getServerName(), - originalMessage, - ChatLogType.PUBLIC, - null, - null, - render(player, inputComponent).asComponent(), - true - ); - return; // the message was blocked - } - - Stream stream = event.viewers().stream().filter(audience -> audience instanceof Player) - .map(audience -> (Player) audience); - - if (!player.hasPermission("chat.ignorebypass")) { - stream = stream.filter(receiver -> { - boolean isPlayerIgnored = ChatUserManager - .getChatUser(receiver.getUniqueId()) - .getIgnoredPlayers() - .contains(uuid); - return !isPlayerIgnored || receiver.hasPermission("chat.ignorebypass"); - }); - } - Set receivers = stream.collect(Collectors.toSet()); - - Set playersToPing = new HashSet<>(); - pingPlayers(playersToPing, modifiableString, player); - - input = render(player, modifiableString.component()); - for (Player receiver : receivers) { - receiver.sendMessage(input); - } - for (Player pingPlayer : playersToPing) { - pingPlayer.playSound(pingPlayer.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1, 1); - } - chatLogHandler.addChatLog(uuid, - ServerName.getServerName(), - modifiableString.string(), - ChatLogType.PUBLIC, - null, - null, - input.asComponent(), - false - ); - ALogger.info(PlainTextComponentSerializer.plainText().serialize(inputComponent)); - } - - private void pingPlayers(Set playersToPing, ModifiableString modifiableString, Player player) { - for (Player onlinePlayer : Bukkit.getOnlinePlayers()) { - String name = onlinePlayer.getName(); - String nickName = PlainTextComponentSerializer.plainText().serialize(onlinePlayer.displayName()); - - Pattern namePattern = Pattern.compile("\\b(? { - // String substring = a.group().substring(1); - // return ; - // }); - - if (!ChatUserManager.getChatUser(onlinePlayer.getUniqueId()) - .getIgnoredPlayers() - .contains(player.getUniqueId()) - || player.hasPermission("chat.ignorebypass")) { - playersToPing.add(onlinePlayer); - } - } else if (nickPattern.matcher(modifiableString.string()).find()) { - modifiableString.replace(TextReplacementConfig.builder() - .once() - .match(nickPattern) - .replacement(mention.append(onlinePlayerUser.getDisplayName())) - .build()); - if (!ChatUserManager.getChatUser(onlinePlayer.getUniqueId()) - .getIgnoredPlayers() - .contains(player.getUniqueId()) - || player.hasPermission("chat.ignorebypass")) { - playersToPing.add(onlinePlayer); - } - } - } - } - - public @NotNull ComponentLike render(@NotNull Player player, @NotNull Component message) { - ChatUser user = ChatUserManager.getChatUser(player.getUniqueId()); - TagResolver placeholders = TagResolver.resolver( - Placeholder.component("sender", user.getDisplayName()), - Placeholder.parsed("sendername", player.getName()), - Placeholder.component("prefix", user.getPrefix()), - Placeholder.component("prefixall", user.getPrefixAll()), - Placeholder.component("staffprefix", user.getStaffPrefix()), - Placeholder.component("message", message) - ); - - return Utility.parseMiniMessage(Config.CHATFORMAT, placeholders); + chatMessageSender.sendMessage(event.getPlayer(), event.message()); } private ComponentLike parseMessageContent(Player player, String rawMessage) { diff --git a/galaxy/src/main/java/com/alttd/chat/util/GalaxyUtility.java b/galaxy/src/main/java/com/alttd/chat/util/GalaxyUtility.java index 419b8a4..615f544 100644 --- a/galaxy/src/main/java/com/alttd/chat/util/GalaxyUtility.java +++ b/galaxy/src/main/java/com/alttd/chat/util/GalaxyUtility.java @@ -6,6 +6,7 @@ import net.kyori.adventure.text.ComponentLike; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import java.util.ArrayList; @@ -13,13 +14,14 @@ import java.util.List; public class GalaxyUtility { - public static void sendBlockedNotification(String prefix, Player player, ComponentLike input, String target) { + public static void sendBlockedNotification(String prefix, OfflinePlayer offlinePlayer, ComponentLike input, String target) { + String playerName = offlinePlayer.getName() == null ? "unknown_player" : offlinePlayer.getName(); TagResolver placeholders = TagResolver.resolver( Placeholder.parsed("prefix", prefix), - Placeholder.parsed("displayname", Utility.getDisplayName(player.getUniqueId(), player.getName())), + Placeholder.parsed("displayname", Utility.getDisplayName(offlinePlayer.getUniqueId(), playerName)), Placeholder.parsed("target", (target.isEmpty() ? "tried to say:" : "-> " + target + ":")), Placeholder.component("input", input) - ); + ); ComponentLike blockedNotification = Utility.parseMiniMessage(Config.NOTIFICATIONFORMAT, placeholders); Bukkit.getOnlinePlayers().forEach(a -> { @@ -27,8 +29,15 @@ public class GalaxyUtility { a.sendMessage(blockedNotification); } }); - player.sendRichMessage("The language you used in your message is not allowed, " + - "this constitutes as your only warning. Any further attempts at bypassing the filter will result in staff intervention."); + if (offlinePlayer.isOnline()) { + Player player = offlinePlayer.getPlayer(); + if (player == null) { + ALogger.error("Player is offline but isOnline() returned true"); + return; + } + player.sendRichMessage("The language you used in your message is not allowed, " + + "this constitutes as your only warning. Any further attempts at bypassing the filter will result in staff intervention."); + } } public static void addAdditionalChatCompletions(Player player) {