From a9d2f4e8ca7f830955e142bbcd40a022640b5e46 Mon Sep 17 00:00:00 2001 From: Kieran Wallbanks Date: Wed, 5 May 2021 18:21:38 +0100 Subject: [PATCH] Respect client chat settings --- .../audience/PacketGroupingAudience.java | 5 +- .../net/minestom/server/entity/Player.java | 32 +++-- .../server/listener/ChatMessageListener.java | 28 ++-- .../server/listener/SettingsListener.java | 2 +- .../server/message/ChatMessageType.java | 65 +++++++++ .../minestom/server/message/ChatPosition.java | 79 +++++++++++ .../minestom/server/message/Messenger.java | 126 ++++++++++++++++++ .../client/play/ClientSettingsPacket.java | 7 +- .../packet/server/play/ChatMessagePacket.java | 56 ++------ .../net/minestom/server/utils/Action.java | 17 +++ 10 files changed, 341 insertions(+), 76 deletions(-) create mode 100644 src/main/java/net/minestom/server/message/ChatMessageType.java create mode 100644 src/main/java/net/minestom/server/message/ChatPosition.java create mode 100644 src/main/java/net/minestom/server/message/Messenger.java create mode 100644 src/main/java/net/minestom/server/utils/Action.java diff --git a/src/main/java/net/minestom/server/adventure/audience/PacketGroupingAudience.java b/src/main/java/net/minestom/server/adventure/audience/PacketGroupingAudience.java index f391c8765..2a2329374 100644 --- a/src/main/java/net/minestom/server/adventure/audience/PacketGroupingAudience.java +++ b/src/main/java/net/minestom/server/adventure/audience/PacketGroupingAudience.java @@ -12,7 +12,8 @@ import net.kyori.adventure.title.Title; import net.minestom.server.MinecraftServer; import net.minestom.server.adventure.AdventurePacketConvertor; import net.minestom.server.entity.Player; -import net.minestom.server.network.packet.server.play.ChatMessagePacket; +import net.minestom.server.message.ChatPosition; +import net.minestom.server.message.Messenger; import net.minestom.server.network.packet.server.play.PlayerListHeaderAndFooterPacket; import net.minestom.server.network.packet.server.play.TitlePacket; import net.minestom.server.utils.PacketUtils; @@ -46,7 +47,7 @@ public interface PacketGroupingAudience extends ForwardingAudience { @Override default void sendMessage(@NotNull Identity source, @NotNull Component message, @NotNull MessageType type) { - PacketUtils.sendGroupedPacket(this.getPlayers(), new ChatMessagePacket(message, ChatMessagePacket.Position.fromMessageType(type), source.uuid())); + Messenger.sendMessage(this.getPlayers(), message, ChatPosition.fromMessageType(type), source.uuid()); } @Override diff --git a/src/main/java/net/minestom/server/entity/Player.java b/src/main/java/net/minestom/server/entity/Player.java index 4ed2c950d..dce6aab68 100644 --- a/src/main/java/net/minestom/server/entity/Player.java +++ b/src/main/java/net/minestom/server/entity/Player.java @@ -38,12 +38,15 @@ import net.minestom.server.event.player.*; import net.minestom.server.instance.Chunk; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.CustomBlock; +import net.minestom.server.message.ChatMessageType; +import net.minestom.server.message.ChatPosition; import net.minestom.server.inventory.Inventory; import net.minestom.server.inventory.PlayerInventory; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.server.item.metadata.WrittenBookMeta; import net.minestom.server.listener.PlayerDiggingListener; +import net.minestom.server.message.Messenger; import net.minestom.server.network.ConnectionManager; import net.minestom.server.network.ConnectionState; import net.minestom.server.network.PlayerProvider; @@ -69,7 +72,6 @@ import net.minestom.server.utils.entity.EntityUtils; import net.minestom.server.utils.identity.NamedAndIdentified; import net.minestom.server.utils.instance.InstanceUtils; import net.minestom.server.utils.inventory.PlayerInventoryUtils; -import net.minestom.server.utils.player.PlayerUtils; import net.minestom.server.utils.time.Cooldown; import net.minestom.server.utils.time.TimeUnit; import net.minestom.server.utils.time.UpdateOption; @@ -739,8 +741,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable, @Override public void sendMessage(@NotNull Identity source, @NotNull Component message, @NotNull MessageType type) { - ChatMessagePacket chatMessagePacket = new ChatMessagePacket(message, ChatMessagePacket.Position.fromMessageType(type), source.uuid()); - playerConnection.sendPacket(chatMessagePacket); + Messenger.sendMessage(this, message, ChatPosition.fromMessageType(type), source.uuid()); } /** @@ -2599,6 +2600,10 @@ public class Player extends LivingEntity implements CommandSender, Localizable, RIGHT } + /** + * @deprecated See {@link ChatMessageType} + */ + @Deprecated public enum ChatMode { ENABLED, COMMANDS_ONLY, @@ -2609,7 +2614,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable, private String locale; private byte viewDistance; - private ChatMode chatMode; + private ChatMessageType chatMessageType; private boolean chatColors; private byte displayedSkinParts; private MainHand mainHand; @@ -2638,7 +2643,16 @@ public class Player extends LivingEntity implements CommandSender, Localizable, * @return the player chat mode */ public ChatMode getChatMode() { - return chatMode; + return ChatMode.values()[chatMessageType.ordinal()]; + } + + /** + * Gets the messages this player wants to receive. + * + * @return the messages + */ + public @NotNull ChatMessageType getChatMessageType() { + return chatMessageType; } /** @@ -2670,19 +2684,19 @@ public class Player extends LivingEntity implements CommandSender, Localizable, * * @param locale the player locale * @param viewDistance the player view distance - * @param chatMode the player chat mode - * @param chatColors the player chat colors + * @param chatMessageType the chat messages the player wishes to receive + * @param chatColors if chat colors should be displayed * @param displayedSkinParts the player displayed skin parts * @param mainHand the player main hand */ - public void refresh(String locale, byte viewDistance, ChatMode chatMode, boolean chatColors, + public void refresh(String locale, byte viewDistance, ChatMessageType chatMessageType, boolean chatColors, byte displayedSkinParts, MainHand mainHand) { final boolean viewDistanceChanged = this.viewDistance != viewDistance; this.locale = locale; this.viewDistance = viewDistance; - this.chatMode = chatMode; + this.chatMessageType = chatMessageType; this.chatColors = chatColors; this.displayedSkinParts = displayedSkinParts; this.mainHand = mainHand; diff --git a/src/main/java/net/minestom/server/listener/ChatMessageListener.java b/src/main/java/net/minestom/server/listener/ChatMessageListener.java index 1f5e56a49..d58525f69 100644 --- a/src/main/java/net/minestom/server/listener/ChatMessageListener.java +++ b/src/main/java/net/minestom/server/listener/ChatMessageListener.java @@ -6,10 +6,10 @@ import net.minestom.server.MinecraftServer; import net.minestom.server.command.CommandManager; import net.minestom.server.entity.Player; import net.minestom.server.event.player.PlayerChatEvent; +import net.minestom.server.message.ChatPosition; +import net.minestom.server.message.Messenger; import net.minestom.server.network.ConnectionManager; import net.minestom.server.network.packet.client.play.ClientChatMessagePacket; -import net.minestom.server.network.packet.server.play.ChatMessagePacket; -import net.minestom.server.utils.PacketUtils; import org.jetbrains.annotations.NotNull; import java.util.Collection; @@ -26,21 +26,26 @@ public class ChatMessageListener { final String cmdPrefix = CommandManager.COMMAND_PREFIX; if (message.startsWith(cmdPrefix)) { // The message is a command - message = message.replaceFirst(cmdPrefix, ""); + final String command = message.replaceFirst(cmdPrefix, ""); - COMMAND_MANAGER.execute(player, message); + // check if we can receive commands + Messenger.receiveCommand(player, () -> COMMAND_MANAGER.execute(player, command)); // Do not call chat event return; } + // check if we can receive messages + if (!Messenger.canReceiveMessage(player)) { + Messenger.sendRejectionMessage(player); + return; + } + final Collection players = CONNECTION_MANAGER.getOnlinePlayers(); - String finalMessage = message; - PlayerChatEvent playerChatEvent = new PlayerChatEvent(player, players, () -> buildDefaultChatMessage(player, finalMessage), message); + PlayerChatEvent playerChatEvent = new PlayerChatEvent(player, players, () -> buildDefaultChatMessage(player, message), message); // Call the event player.callCancellableEvent(PlayerChatEvent.class, playerChatEvent, () -> { - final Function formatFunction = playerChatEvent.getChatFormatFunction(); Component textObject; @@ -55,15 +60,10 @@ public class ChatMessageListener { final Collection recipients = playerChatEvent.getRecipients(); if (!recipients.isEmpty()) { - // Send the message with the correct player UUID - ChatMessagePacket chatMessagePacket = - new ChatMessagePacket(textObject, ChatMessagePacket.Position.CHAT, player.getUuid()); - - PacketUtils.sendGroupedPacket(recipients, chatMessagePacket); + // delegate to the messenger to avoid sending messages we shouldn't be + Messenger.sendMessage(recipients, textObject, ChatPosition.CHAT, player.getUuid()); } - }); - } private static @NotNull Component buildDefaultChatMessage(@NotNull Player player, @NotNull String message) { diff --git a/src/main/java/net/minestom/server/listener/SettingsListener.java b/src/main/java/net/minestom/server/listener/SettingsListener.java index 7f23840be..3099a8226 100644 --- a/src/main/java/net/minestom/server/listener/SettingsListener.java +++ b/src/main/java/net/minestom/server/listener/SettingsListener.java @@ -8,7 +8,7 @@ public class SettingsListener { public static void listener(ClientSettingsPacket packet, Player player) { Player.PlayerSettings settings = player.getSettings(); - settings.refresh(packet.locale, packet.viewDistance, packet.chatMode, packet.chatColors, packet.displayedSkinParts, packet.mainHand); + settings.refresh(packet.locale, packet.viewDistance, packet.chatMessageType, packet.chatColors, packet.displayedSkinParts, packet.mainHand); PlayerSettingsChangeEvent playerSettingsChangeEvent = new PlayerSettingsChangeEvent(player); player.callEvent(PlayerSettingsChangeEvent.class, playerSettingsChangeEvent); diff --git a/src/main/java/net/minestom/server/message/ChatMessageType.java b/src/main/java/net/minestom/server/message/ChatMessageType.java new file mode 100644 index 000000000..73bf2fffd --- /dev/null +++ b/src/main/java/net/minestom/server/message/ChatMessageType.java @@ -0,0 +1,65 @@ +package net.minestom.server.message; + +import org.jetbrains.annotations.NotNull; + +import java.util.EnumSet; + +/** + * The messages that a player is willing to receive. + */ +public enum ChatMessageType { + /** + * The client wants all chat messages. + */ + FULL(EnumSet.allOf(ChatPosition.class)), + + /** + * The client only wants messages from commands, or system messages. + */ + SYSTEM(EnumSet.of(ChatPosition.SYSTEM_MESSAGE, ChatPosition.GAME_INFO)), + + /** + * The client doesn't want any messages. + */ + NONE(EnumSet.of(ChatPosition.GAME_INFO)); + + private final EnumSet acceptedPositions; + + ChatMessageType(@NotNull EnumSet acceptedPositions) { + this.acceptedPositions = acceptedPositions; + } + + /** + * Checks if this message type is accepting of messages from a given position. + * + * @param chatPosition the position + * @return if the message is accepted + */ + public boolean accepts(@NotNull ChatPosition chatPosition) { + return this.acceptedPositions.contains(chatPosition); + } + + /** + * Gets the packet ID for this chat message type. + * + * @return the packet ID + */ + public int getPacketID() { + return this.ordinal(); + } + + /** + * Gets a chat message type from a packet ID. + * + * @param id the packet ID + * @return the chat message type + */ + public static @NotNull ChatMessageType fromPacketID(int id) { + switch (id) { + case 0: return FULL; + case 1: return SYSTEM; + case 2: return NONE; + default: throw new IllegalArgumentException("id must be between 0-2 (inclusive)"); + } + } +} diff --git a/src/main/java/net/minestom/server/message/ChatPosition.java b/src/main/java/net/minestom/server/message/ChatPosition.java new file mode 100644 index 000000000..345fa0677 --- /dev/null +++ b/src/main/java/net/minestom/server/message/ChatPosition.java @@ -0,0 +1,79 @@ +package net.minestom.server.message; + +import net.kyori.adventure.audience.MessageType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * The different positions for chat messages. + */ +public enum ChatPosition { + /** + * A player-initiated chat message. + */ + CHAT(MessageType.CHAT), + + /** + * Feedback from running a command or other system messages. + */ + SYSTEM_MESSAGE(MessageType.SYSTEM), + + /** + * Game state information displayed above the hot bar. + */ + GAME_INFO(null); + + private final MessageType messageType; + + ChatPosition(@NotNull MessageType messageType) { + this.messageType = messageType; + } + + /** + * Gets the Adventure message type from this position. Note that there is no + * message type for {@link #GAME_INFO}, as Adventure uses the title methods for this. + * + * @return the message type, if any + */ + public @Nullable MessageType getMessageType() { + return this.messageType; + } + + /** + * Gets the packet ID of this chat position. + * + * @return the ID + */ + public byte getID() { + return (byte) this.ordinal(); + } + + /** + * Gets a position from an Adventure message type. + * + * @param messageType the message type + * @return the position + */ + public static @NotNull ChatPosition fromMessageType(@NotNull MessageType messageType) { + switch (messageType) { + case CHAT: return CHAT; + case SYSTEM: return SYSTEM_MESSAGE; + } + throw new IllegalArgumentException("Cannot get position from message type!"); + } + + /** + * Gets a position from a packet ID. + * + * @param id the id + * @return the chat position + */ + public static @NotNull ChatPosition fromPacketID(byte id) { + switch (id) { + case 0: return CHAT; + case 1: return SYSTEM_MESSAGE; + case 2: return GAME_INFO; + default: throw new IllegalArgumentException("id must be between 0-2 (inclusive)"); + } + } +} diff --git a/src/main/java/net/minestom/server/message/Messenger.java b/src/main/java/net/minestom/server/message/Messenger.java new file mode 100644 index 000000000..0e5e69bfc --- /dev/null +++ b/src/main/java/net/minestom/server/message/Messenger.java @@ -0,0 +1,126 @@ +package net.minestom.server.message; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minestom.server.entity.Player; +import net.minestom.server.network.packet.server.play.ChatMessagePacket; +import net.minestom.server.utils.Action; +import net.minestom.server.utils.PacketUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +/** + * Utility class to handle client chat settings. + */ +public class Messenger { + /** + * The message sent to the client if they send a chat message but it is rejected by the server. + */ + public static final Component CANNOT_SEND_MESSAGE = Component.translatable("chat.cannotSend", NamedTextColor.RED); + + private static final ChatMessagePacket CANNOT_SEND_PACKET = new ChatMessagePacket(CANNOT_SEND_MESSAGE, ChatPosition.SYSTEM_MESSAGE, null); + + /** + * Sends a message to a player, respecting their chat settings. + * + * @param player the player + * @param message the message + * @param position the position + * @param uuid the UUID of the sender, if any + * @return if the message was sent + */ + public static boolean sendMessage(@NotNull Player player, @NotNull Component message, @NotNull ChatPosition position, @Nullable UUID uuid) { + if (player.getSettings().getChatMessageType().accepts(position)) { + player.getPlayerConnection().sendPacket(new ChatMessagePacket(message, position, uuid)); + return true; + } + + return false; + } + + /** + * Sends a message to some players, respecting their chat settings. + * + * @param players the players + * @param message the message + * @param position the position + * @param uuid the UUID of the sender, if any + * @return a set of players who received the message + */ + public static @NotNull Set sendMessage(@NotNull Iterable players, @NotNull Component message, + @NotNull ChatPosition position, @Nullable UUID uuid) { + final Set sentTo = new HashSet<>(); + + for (Player player : players) { + if (player.getSettings().getChatMessageType().accepts(position)) { + sentTo.add(player); + } + } + + PacketUtils.sendGroupedPacket(sentTo, new ChatMessagePacket(message, position, uuid)); + return sentTo; + } + + /** + * Checks if the server should receive messages from a player, given their chat settings. + * + * @param player the player + * @return if the server should receive messages from them + */ + public static boolean canReceiveMessage(@NotNull Player player) { + return player.getSettings().getChatMessageType() == ChatMessageType.FULL; + } + + /** + * Performs an action if the server can receive messages from a player. + * This method will send the rejection message automatically. + * + * @param player the player + * @param action the action + */ + public static void receiveMessage(@NotNull Player player, @NotNull Action action) { + if (canReceiveMessage(player)) { + action.act(); + } else { + sendRejectionMessage(player); + } + } + + /** + * Checks if the server should receive commands from a player, given their chat settings. + * + * @param player the player + * @return if the server should receive commands from them + */ + public static boolean canReceiveCommand(@NotNull Player player) { + return player.getSettings().getChatMessageType() != ChatMessageType.NONE; + } + + /** + * Performs an action if the server can receive commands from a player. + * This method will send the rejection message automatically. + * + * @param player the player + * @param action the action + */ + public static void receiveCommand(@NotNull Player player, @NotNull Action action) { + if (canReceiveCommand(player)) { + action.act(); + } else { + sendRejectionMessage(player); + } + } + + /** + * Sends a message to the player informing them we are rejecting their message or command. + * + * @param player the player + */ + public static void sendRejectionMessage(@NotNull Player player) { + player.getPlayerConnection().sendPacket(CANNOT_SEND_PACKET); + } +} diff --git a/src/main/java/net/minestom/server/network/packet/client/play/ClientSettingsPacket.java b/src/main/java/net/minestom/server/network/packet/client/play/ClientSettingsPacket.java index 968216cc9..8af3da577 100644 --- a/src/main/java/net/minestom/server/network/packet/client/play/ClientSettingsPacket.java +++ b/src/main/java/net/minestom/server/network/packet/client/play/ClientSettingsPacket.java @@ -1,6 +1,7 @@ package net.minestom.server.network.packet.client.play; import net.minestom.server.entity.Player; +import net.minestom.server.message.ChatMessageType; import net.minestom.server.network.packet.client.ClientPlayPacket; import net.minestom.server.utils.binary.BinaryReader; import net.minestom.server.utils.binary.BinaryWriter; @@ -10,7 +11,7 @@ public class ClientSettingsPacket extends ClientPlayPacket { public String locale = ""; public byte viewDistance; - public Player.ChatMode chatMode = Player.ChatMode.ENABLED; + public ChatMessageType chatMessageType = ChatMessageType.FULL; public boolean chatColors; public byte displayedSkinParts; public Player.MainHand mainHand = Player.MainHand.RIGHT; @@ -19,7 +20,7 @@ public class ClientSettingsPacket extends ClientPlayPacket { public void read(@NotNull BinaryReader reader) { this.locale = reader.readSizedString(128); this.viewDistance = reader.readByte(); - this.chatMode = Player.ChatMode.values()[reader.readVarInt()]; + this.chatMessageType = ChatMessageType.fromPacketID(reader.readVarInt()); this.chatColors = reader.readBoolean(); this.displayedSkinParts = reader.readByte(); this.mainHand = Player.MainHand.values()[reader.readVarInt()]; @@ -31,7 +32,7 @@ public class ClientSettingsPacket extends ClientPlayPacket { throw new IllegalArgumentException("Locale cannot be longer than 128 characters."); writer.writeSizedString(locale); writer.writeByte(viewDistance); - writer.writeVarInt(chatMode.ordinal()); + writer.writeVarInt(chatMessageType.getPacketID()); writer.writeBoolean(chatColors); writer.writeByte(displayedSkinParts); writer.writeVarInt(mainHand.ordinal()); diff --git a/src/main/java/net/minestom/server/network/packet/server/play/ChatMessagePacket.java b/src/main/java/net/minestom/server/network/packet/server/play/ChatMessagePacket.java index cc9f57195..246edb55d 100644 --- a/src/main/java/net/minestom/server/network/packet/server/play/ChatMessagePacket.java +++ b/src/main/java/net/minestom/server/network/packet/server/play/ChatMessagePacket.java @@ -1,7 +1,7 @@ package net.minestom.server.network.packet.server.play; -import net.kyori.adventure.audience.MessageType; import net.kyori.adventure.text.Component; +import net.minestom.server.message.ChatPosition; import net.minestom.server.network.packet.server.ComponentHoldingServerPacket; import net.minestom.server.network.packet.server.ServerPacket; import net.minestom.server.network.packet.server.ServerPacketIdentifier; @@ -12,6 +12,7 @@ import org.jetbrains.annotations.Nullable; import java.util.Collection; import java.util.Collections; +import java.util.Objects; import java.util.UUID; import java.util.function.UnaryOperator; @@ -22,21 +23,19 @@ public class ChatMessagePacket implements ComponentHoldingServerPacket { private static final UUID NULL_UUID = new UUID(0, 0); public Component message; - public Position position; + public ChatPosition position; public UUID uuid; public ChatMessagePacket() { - this(Component.empty(), Position.CHAT); + this.message = Component.empty(); + this.position = ChatPosition.SYSTEM_MESSAGE; + this.uuid = NULL_UUID; } - public ChatMessagePacket(Component message, Position position, UUID uuid) { + public ChatMessagePacket(@NotNull Component message, @NotNull ChatPosition position, @Nullable UUID uuid) { this.message = message; this.position = position; - this.uuid = uuid; - } - - public ChatMessagePacket(Component message, Position position) { - this(message, position, NULL_UUID); + this.uuid = Objects.requireNonNullElse(uuid, NULL_UUID); } @Override @@ -49,7 +48,7 @@ public class ChatMessagePacket implements ComponentHoldingServerPacket { @Override public void read(@NotNull BinaryReader reader) { message = reader.readComponent(Integer.MAX_VALUE); - position = Position.values()[reader.readByte()]; + position = ChatPosition.fromPacketID(reader.readByte()); uuid = reader.readUuid(); } @@ -67,41 +66,4 @@ public class ChatMessagePacket implements ComponentHoldingServerPacket { public @NotNull ServerPacket copyWithOperator(@NotNull UnaryOperator operator) { return new ChatMessagePacket(operator.apply(message), position, uuid); } - - public enum Position { - CHAT(MessageType.CHAT), - SYSTEM_MESSAGE(MessageType.SYSTEM), - GAME_INFO(null); - - private final MessageType messageType; - - Position(MessageType messageType) { - this.messageType = messageType; - } - - /** - * Gets the Adventure message type from this position. Note that there is no - * message type for {@link #GAME_INFO}, as Adventure uses the title methods for this. - * - * @return the message type, if any - */ - public @Nullable MessageType getMessageType() { - return this.messageType; - } - - /** - * Gets a position from an Adventure message type. - * - * @param messageType the message type - * - * @return the position - */ - public static @NotNull Position fromMessageType(@NotNull MessageType messageType) { - switch (messageType) { - case CHAT: return CHAT; - case SYSTEM: return SYSTEM_MESSAGE; - } - throw new IllegalArgumentException("Cannot get position from message type!"); - } - } } diff --git a/src/main/java/net/minestom/server/utils/Action.java b/src/main/java/net/minestom/server/utils/Action.java new file mode 100644 index 000000000..e2fda399b --- /dev/null +++ b/src/main/java/net/minestom/server/utils/Action.java @@ -0,0 +1,17 @@ +package net.minestom.server.utils; + +/** + * A functional interface to perform an action. + */ +@FunctionalInterface +public interface Action { + /** + * An empty action. + */ + Action EMPTY = () -> {}; + + /** + * Performs the action. + */ + void act(); +}