From 1ee8be400a8b42f9cafde14a50e3c30d0d3ef4c7 Mon Sep 17 00:00:00 2001
From: Kevin <92656833+kevinthegreat1@users.noreply.github.com>
Date: Thu, 23 Feb 2023 05:13:47 -0500
Subject: [PATCH] Added Client Message Events (#2646)

* Added Client Message Events

* Applied suggestions and fixed checkstyle

* Inject before fabric-command-api and updated Javadocs

* Updated Javadocs regarding client commands

* Update fabric-message-api-v1/src/client/resources/fabric-message-api-v1.client.mixins.json

Co-authored-by: Juuz <6596629+Juuxel@users.noreply.github.com>

* Updated Javadocs regarding commands

* Fixed duplicated package names

* Updated ClientMessageEvents.java Javadoc

Co-authored-by: Sideroo <109681866+Sideroo@users.noreply.github.com>

* Removed duplicated client commands Javadoc

* Added cancelled sending and receiving events

* Seperated send and receive events and changed event names

* Fixed checkstyle

* Added support for modifying messages

* Added client command test

* Added narration and message indicator support for modifying received messages

* Added tests for modifying messages

* Updated ClientReceiveMessageEvents#CHAT Javadocs

* Small Javadoc fixes

* Added Modify to names

* Always narrate original message

* Removed modifying receive chat message

* Split notify and modify events

* Fixed checkstyle

---------

Co-authored-by: Juuz <6596629+Juuxel@users.noreply.github.com>
Co-authored-by: Sideroo <109681866+Sideroo@users.noreply.github.com>
(cherry picked from commit c85585f8702861e559f9da6928a4f60e9a2dbbc8)
---
 fabric-message-api-v1/build.gradle            |   4 +
 .../v1/ClientReceiveMessageEvents.java        | 259 ++++++++++++++++++
 .../message/v1/ClientSendMessageEvents.java   | 251 +++++++++++++++++
 .../ClientPlayNetworkHandlerMixin.java        |  64 +++++
 .../client/message/MessageHandlerMixin.java   |  83 ++++++
 .../fabric-message-api-v1.client.mixins.json  |  12 +
 .../src/main/resources/fabric.mod.json        |   6 +-
 .../fabric/test/message/ChatTestClient.java   | 108 ++++++++
 .../src/testmod/resources/fabric.mod.json     |   6 +-
 9 files changed, 791 insertions(+), 2 deletions(-)
 create mode 100644 fabric-message-api-v1/src/client/java/net/fabricmc/fabric/api/client/message/v1/ClientReceiveMessageEvents.java
 create mode 100644 fabric-message-api-v1/src/client/java/net/fabricmc/fabric/api/client/message/v1/ClientSendMessageEvents.java
 create mode 100644 fabric-message-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/message/ClientPlayNetworkHandlerMixin.java
 create mode 100644 fabric-message-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/message/MessageHandlerMixin.java
 create mode 100644 fabric-message-api-v1/src/client/resources/fabric-message-api-v1.client.mixins.json
 create mode 100644 fabric-message-api-v1/src/testmod/java/net/fabricmc/fabric/test/message/ChatTestClient.java

diff --git a/fabric-message-api-v1/build.gradle b/fabric-message-api-v1/build.gradle
index ebfd97c13..ae3fd953c 100644
--- a/fabric-message-api-v1/build.gradle
+++ b/fabric-message-api-v1/build.gradle
@@ -4,3 +4,7 @@ version = getSubprojectVersion(project)
 moduleDependencies(project, [
 		'fabric-api-base'
 ])
+
+testDependencies(project, [
+		'fabric-command-api-v2'
+])
diff --git a/fabric-message-api-v1/src/client/java/net/fabricmc/fabric/api/client/message/v1/ClientReceiveMessageEvents.java b/fabric-message-api-v1/src/client/java/net/fabricmc/fabric/api/client/message/v1/ClientReceiveMessageEvents.java
new file mode 100644
index 000000000..3abd8180c
--- /dev/null
+++ b/fabric-message-api-v1/src/client/java/net/fabricmc/fabric/api/client/message/v1/ClientReceiveMessageEvents.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.api.client.message.v1;
+
+import java.time.Instant;
+
+import com.mojang.authlib.GameProfile;
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.client.gui.hud.ChatHud;
+import net.minecraft.network.message.MessageType;
+import net.minecraft.network.message.SignedMessage;
+import net.minecraft.text.Text;
+
+import net.fabricmc.fabric.api.event.Event;
+import net.fabricmc.fabric.api.event.EventFactory;
+
+/**
+ * Contains client-side events triggered when receiving messages.
+ */
+public final class ClientReceiveMessageEvents {
+	private ClientReceiveMessageEvents() {
+	}
+
+	/**
+	 * An event triggered when the client receives a chat message,
+	 * which is any message sent by a player. Mods can use this to block the message.
+	 *
+	 * <p>If a listener returned {@code false}, the message will not be displayed,
+	 * the remaining listeners will not be called (if any), and
+	 * {@link #CHAT_CANCELED} will be triggered instead of {@link #CHAT}.
+	 */
+	public static final Event<AllowChat> ALLOW_CHAT = EventFactory.createArrayBacked(AllowChat.class, listeners -> (message, signedMessage, sender, params, receptionTimestamp) -> {
+		for (AllowChat listener : listeners) {
+			if (!listener.allowReceiveChatMessage(message, signedMessage, sender, params, receptionTimestamp)) {
+				return false;
+			}
+		}
+
+		return true;
+	});
+
+	/**
+	 * An event triggered when the client receives a game message,
+	 * which is any message sent by the server.
+	 * Mods can use this to block the message or toggle overlay.
+	 *
+	 * <p>If a listener returned {@code false}, the message will not be displayed,
+	 * the remaining listeners will not be called (if any), and
+	 * {@link #GAME_CANCELED} will be triggered instead of {@link #MODIFY_GAME}.
+	 *
+	 * <p>Overlay is whether the message will be displayed in the action bar.
+	 * To toggle overlay, return false and call
+	 * {@link net.minecraft.client.network.ClientPlayerEntity#sendMessage(Text, boolean) ClientPlayerEntity.sendMessage(message, overlay)}.
+	 */
+	public static final Event<AllowGame> ALLOW_GAME = EventFactory.createArrayBacked(AllowGame.class, listeners -> (message, overlay) -> {
+		for (AllowGame listener : listeners) {
+			if (!listener.allowReceiveGameMessage(message, overlay)) {
+				return false;
+			}
+		}
+
+		return true;
+	});
+
+	/**
+	 * An event triggered when the client receives a game message,
+	 * which is any message sent by the server. Is not called when
+	 * {@linkplain #ALLOW_GAME game messages are blocked}.
+	 * Mods can use this to modify the message.
+	 * Use {@link #GAME} if not modifying the message.
+	 *
+	 * <p>Overlay is whether the message will be displayed in the action bar.
+	 * Use {@link #ALLOW_GAME to toggle overlay}.
+	 */
+	public static final Event<ModifyGame> MODIFY_GAME = EventFactory.createArrayBacked(ModifyGame.class, listeners -> (message, overlay) -> {
+		for (ModifyGame listener : listeners) {
+			message = listener.modifyReceivedGameMessage(message, overlay);
+		}
+
+		return message;
+	});
+
+	/**
+	 * An event triggered when the client receives a chat message,
+	 * which is any message sent by a player. Is not called when
+	 * {@linkplain #ALLOW_CHAT chat messages are blocked}.
+	 * Mods can use this to listen to the message.
+	 *
+	 * <p>If mods want to modify the message, they should use {@link #ALLOW_CHAT}
+	 * and manually add the new message to the chat hud using {@link ChatHud#addMessage(Text)}
+	 */
+	public static final Event<Chat> CHAT = EventFactory.createArrayBacked(Chat.class, listeners -> (message, signedMessage, sender, params, receptionTimestamp) -> {
+		for (Chat listener : listeners) {
+			listener.onReceiveChatMessage(message, signedMessage, sender, params, receptionTimestamp);
+		}
+	});
+
+	/**
+	 * An event triggered when the client receives a game message,
+	 * which is any message sent by the server. Is not called when
+	 * {@linkplain #ALLOW_GAME game messages are blocked}.
+	 * Mods can use this to listen to the message.
+	 *
+	 * <p>Overlay is whether the message will be displayed in the action bar.
+	 * Use {@link #ALLOW_GAME to toggle overlay}.
+	 */
+	public static final Event<Game> GAME = EventFactory.createArrayBacked(Game.class, listeners -> (message, overlay) -> {
+		for (Game listener : listeners) {
+			listener.onReceiveGameMessage(message, overlay);
+		}
+	});
+
+	/**
+	 * An event triggered when receiving a chat message is canceled with {@link #ALLOW_CHAT}.
+	 */
+	public static final Event<ChatCanceled> CHAT_CANCELED = EventFactory.createArrayBacked(ChatCanceled.class, listeners -> (message, signedMessage, sender, params, receptionTimestamp) -> {
+		for (ChatCanceled listener : listeners) {
+			listener.onReceiveChatMessageCanceled(message, signedMessage, sender, params, receptionTimestamp);
+		}
+	});
+
+	/**
+	 * An event triggered when receiving a game message is canceled with {@link #ALLOW_GAME}.
+	 *
+	 * <p>Overlay is whether the message would have been displayed in the action bar.
+	 */
+	public static final Event<GameCanceled> GAME_CANCELED = EventFactory.createArrayBacked(GameCanceled.class, listeners -> (message, overlay) -> {
+		for (GameCanceled listener : listeners) {
+			listener.onReceiveGameMessageCanceled(message, overlay);
+		}
+	});
+
+	@FunctionalInterface
+	public interface AllowChat {
+		/**
+		 * Called when the client receives a chat message,
+		 * which is any message sent by a player.
+		 * Returning {@code false} prevents the message from being displayed, and
+		 * {@link #CHAT_CANCELED} will be triggered instead of {@link #CHAT}.
+		 *
+		 * @param message            the message received from the server
+		 * @param signedMessage      the signed message received from the server (nullable)
+		 * @param sender             the sender of the message (nullable)
+		 * @param params             the parameters of the message
+		 * @param receptionTimestamp the timestamp when the message was received
+		 * @return {@code true} if the message should be displayed, otherwise {@code false}
+		 */
+		boolean allowReceiveChatMessage(Text message, @Nullable SignedMessage signedMessage, @Nullable GameProfile sender, MessageType.Parameters params, Instant receptionTimestamp);
+	}
+
+	@FunctionalInterface
+	public interface AllowGame {
+		/**
+		 * Called when the client receives a game message,
+		 * which is any message sent by the server. Returning {@code false}
+		 * prevents the message from being displayed, and
+		 * {@link #GAME_CANCELED} will be triggered instead of {@link #MODIFY_GAME}.
+		 *
+		 * <p>Overlay is whether the message will be displayed in the action bar.
+		 * To toggle overlay, return false and call
+		 * {@link net.minecraft.client.network.ClientPlayerEntity#sendMessage(Text, boolean) ClientPlayerEntity.sendMessage(message, overlay)}.
+		 *
+		 * @param message the message received from the server
+		 * @param overlay whether the message will be displayed in the action bar
+		 * @return {@code true} if the message should be displayed, otherwise {@code false}
+		 */
+		boolean allowReceiveGameMessage(Text message, boolean overlay);
+	}
+
+	@FunctionalInterface
+	public interface ModifyGame {
+		/**
+		 * Called when the client receives a game message,
+		 * which is any message sent by the server. Is not called when
+		 * {@linkplain #ALLOW_GAME game messages are blocked}.
+		 * Use {@link #GAME} if not modifying the message.
+		 *
+		 * <p>Overlay is whether the message will be displayed in the action bar.
+		 * Use {@link #ALLOW_GAME} to toggle overlay.
+		 *
+		 * @param message the message received from the server
+		 * @param overlay whether the message will be displayed in the action bar
+		 * @return the modified message to display or the original {@code message} if the message is not modified
+		 */
+		Text modifyReceivedGameMessage(Text message, boolean overlay);
+	}
+
+	@FunctionalInterface
+	public interface Chat {
+		/**
+		 * Called when the client receives a chat message,
+		 * which is any message sent by a player. Is not called when
+		 * {@linkplain #ALLOW_CHAT chat messages are blocked}.
+		 *
+		 * @param message            the message received from the server
+		 * @param signedMessage      the signed message received from the server (nullable)
+		 * @param sender             the sender of the message (nullable)
+		 * @param params             the parameters of the message
+		 * @param receptionTimestamp the timestamp when the message was received
+		 */
+		void onReceiveChatMessage(Text message, @Nullable SignedMessage signedMessage, @Nullable GameProfile sender, MessageType.Parameters params, Instant receptionTimestamp);
+	}
+
+	@FunctionalInterface
+	public interface Game {
+		/**
+		 * Called when the client receives a game message,
+		 * which is any message sent by the server. Is not called when
+		 * {@linkplain #ALLOW_GAME game messages are blocked}.
+		 *
+		 * <p>Overlay is whether the message will be displayed in the action bar.
+		 * Use {@link #ALLOW_GAME} to toggle overlay.
+		 *
+		 * @param message the message received from the server
+		 * @param overlay whether the message will be displayed in the action bar
+		 */
+		void onReceiveGameMessage(Text message, boolean overlay);
+	}
+
+	@FunctionalInterface
+	public interface ChatCanceled {
+		/**
+		 * Called when receiving a chat message is canceled with {@link #ALLOW_CHAT}.
+		 *
+		 * @param message            the message received from the server
+		 * @param signedMessage      the signed message received from the server (nullable)
+		 * @param sender             the sender of the message (nullable)
+		 * @param params             the parameters of the message
+		 * @param receptionTimestamp the timestamp when the message was received
+		 */
+		void onReceiveChatMessageCanceled(Text message, @Nullable SignedMessage signedMessage, @Nullable GameProfile sender, MessageType.Parameters params, Instant receptionTimestamp);
+	}
+
+	@FunctionalInterface
+	public interface GameCanceled {
+		/**
+		 * Called when receiving a game message is canceled with {@link #ALLOW_GAME}.
+		 *
+		 * @param message the message received from the server
+		 * @param overlay whether the message would have been displayed in the action bar
+		 */
+		void onReceiveGameMessageCanceled(Text message, boolean overlay);
+	}
+}
diff --git a/fabric-message-api-v1/src/client/java/net/fabricmc/fabric/api/client/message/v1/ClientSendMessageEvents.java b/fabric-message-api-v1/src/client/java/net/fabricmc/fabric/api/client/message/v1/ClientSendMessageEvents.java
new file mode 100644
index 000000000..e16904169
--- /dev/null
+++ b/fabric-message-api-v1/src/client/java/net/fabricmc/fabric/api/client/message/v1/ClientSendMessageEvents.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.api.client.message.v1;
+
+import net.fabricmc.fabric.api.event.Event;
+import net.fabricmc.fabric.api.event.EventFactory;
+
+/**
+ * Contains client-side events triggered when sending messages.
+ */
+public final class ClientSendMessageEvents {
+	private ClientSendMessageEvents() {
+	}
+
+	/**
+	 * An event triggered when the client is about to send a chat message,
+	 * typically from a client GUI. Mods can use this to block the message.
+	 *
+	 * <p>If a listener returned {@code false}, the message will not be sent,
+	 * the remaining listeners will not be called (if any), and
+	 * {@link #CHAT_CANCELED} will be triggered instead of {@link #MODIFY_CHAT}.
+	 */
+	public static final Event<AllowChat> ALLOW_CHAT = EventFactory.createArrayBacked(AllowChat.class, listeners -> (message) -> {
+		for (AllowChat listener : listeners) {
+			if (!listener.allowSendChatMessage(message)) {
+				return false;
+			}
+		}
+
+		return true;
+	});
+
+	/**
+	 * An event triggered when the client is about to send a command,
+	 * which is whenever the player executes a command
+	 * including client commands registered with {@code fabric-command-api}.
+	 * Mods can use this to block the message.
+	 * The command string does not include a slash at the beginning.
+	 *
+	 * <p>If a listener returned {@code false}, the command will not be sent,
+	 * the remaining listeners will not be called (if any), and
+	 * {@link #COMMAND_CANCELED} will be triggered instead of {@link #MODIFY_COMMAND}.
+	 */
+	public static final Event<AllowCommand> ALLOW_COMMAND = EventFactory.createArrayBacked(AllowCommand.class, listeners -> (command) -> {
+		for (AllowCommand listener : listeners) {
+			if (!listener.allowSendCommandMessage(command)) {
+				return false;
+			}
+		}
+
+		return true;
+	});
+
+	/**
+	 * An event triggered when the client sends a chat message,
+	 * typically from a client GUI. Is not called when {@linkplain
+	 * #ALLOW_CHAT chat messages are blocked}.
+	 * Mods can use this to modify the message.
+	 * Use {@link #CHAT} if not modifying the message.
+	 */
+	public static final Event<ModifyChat> MODIFY_CHAT = EventFactory.createArrayBacked(ModifyChat.class, listeners -> (message) -> {
+		for (ModifyChat listener : listeners) {
+			message = listener.modifySendChatMessage(message);
+		}
+
+		return message;
+	});
+
+	/**
+	 * An event triggered when the client sends a command,
+	 * which is whenever the player executes a command
+	 * including client commands registered with {@code fabric-command-api}.
+	 * Is not called when {@linkplain #ALLOW_COMMAND command messages are blocked}.
+	 * The command string does not include a slash at the beginning.
+	 * Mods can use this to modify the command.
+	 * Use {@link #COMMAND} if not modifying the command.
+	 */
+	public static final Event<ModifyCommand> MODIFY_COMMAND = EventFactory.createArrayBacked(ModifyCommand.class, listeners -> (command) -> {
+		for (ModifyCommand listener : listeners) {
+			command = listener.modifySendCommandMessage(command);
+		}
+
+		return command;
+	});
+
+	/**
+	 * An event triggered when the client sends a chat message,
+	 * typically from a client GUI. Is not called when {@linkplain
+	 * #ALLOW_CHAT chat messages are blocked}.
+	 * Mods can use this to listen to the message.
+	 */
+	public static final Event<Chat> CHAT = EventFactory.createArrayBacked(Chat.class, listeners -> (message) -> {
+		for (Chat listener : listeners) {
+			listener.onSendChatMessage(message);
+		}
+	});
+
+	/**
+	 * An event triggered when the client sends a command,
+	 * which is whenever the player executes a command
+	 * including client commands registered with {@code fabric-command-api}.
+	 * Is not called when {@linkplain #ALLOW_COMMAND command messages are blocked}.
+	 * The command string does not include a slash at the beginning.
+	 * Mods can use this to listen to the command.
+	 */
+	public static final Event<Command> COMMAND = EventFactory.createArrayBacked(Command.class, listeners -> (command) -> {
+		for (Command listener : listeners) {
+			listener.onSendCommandMessage(command);
+		}
+	});
+
+	/**
+	 * An event triggered when sending a chat message is canceled with {@link #ALLOW_CHAT}.
+	 */
+	public static final Event<ChatCanceled> CHAT_CANCELED = EventFactory.createArrayBacked(ChatCanceled.class, listeners -> (message) -> {
+		for (ChatCanceled listener : listeners) {
+			listener.onSendChatMessageCanceled(message);
+		}
+	});
+
+	/**
+	 * An event triggered when sending a command is canceled with {@link #ALLOW_COMMAND}.
+	 * The command string does not include a slash at the beginning.
+	 */
+	public static final Event<CommandCanceled> COMMAND_CANCELED = EventFactory.createArrayBacked(CommandCanceled.class, listeners -> (command) -> {
+		for (CommandCanceled listener : listeners) {
+			listener.onSendCommandMessageCanceled(command);
+		}
+	});
+
+	@FunctionalInterface
+	public interface AllowChat {
+		/**
+		 * Called when the client is about to send a chat message,
+		 * typically from a client GUI. Returning {@code false}
+		 * prevents the message from being sent, and
+		 * {@link #CHAT_CANCELED} will be triggered instead of {@link #MODIFY_CHAT}.
+		 *
+		 * @param message the message that will be sent to the server
+		 * @return {@code true} if the message should be sent, otherwise {@code false}
+		 */
+		boolean allowSendChatMessage(String message);
+	}
+
+	@FunctionalInterface
+	public interface AllowCommand {
+		/**
+		 * Called when the client is about to send a command,
+		 * which is whenever the player executes a command
+		 * including client commands registered with {@code fabric-command-api}.
+		 * Returning {@code false} prevents the command from being sent, and
+		 * {@link #COMMAND_CANCELED} will be triggered instead of {@link #MODIFY_COMMAND}.
+		 * The command string does not include a slash at the beginning.
+		 *
+		 * @param command the command that will be sent to the server, without a slash at the beginning.
+		 * @return {@code true} if the command should be sent, otherwise {@code false}
+		 */
+		boolean allowSendCommandMessage(String command);
+	}
+
+	@FunctionalInterface
+	public interface ModifyChat {
+		/**
+		 * Called when the client sends a chat message,
+		 * typically from a client GUI. Is not called when {@linkplain
+		 * #ALLOW_CHAT chat messages are blocked}.
+		 * Use {@link #CHAT} if not modifying the message.
+		 *
+		 * @param message the message that will be sent to the server
+		 * @return the modified message that will be sent to the server
+		 */
+		String modifySendChatMessage(String message);
+	}
+
+	@FunctionalInterface
+	public interface ModifyCommand {
+		/**
+		 * Called when the client sends a command,
+		 * which is whenever the player executes a command
+		 * including client commands registered with {@code fabric-command-api}.
+		 * Is not called when {@linkplain #ALLOW_COMMAND command messages are blocked}.
+		 * The command string does not include a slash at the beginning.
+		 * Use {@link #COMMAND} if not modifying the command.
+		 *
+		 * @param command the command that will be sent to the server, without a slash at the beginning.
+		 * @return the modified command that will be sent to the server, without a slash at the beginning.
+		 */
+		String modifySendCommandMessage(String command);
+	}
+
+	@FunctionalInterface
+	public interface Chat {
+		/**
+		 * Called when the client sends a chat message,
+		 * typically from a client GUI. Is not called when {@linkplain
+		 * #ALLOW_CHAT chat messages are blocked}.
+		 *
+		 * @param message the message that will be sent to the server
+		 */
+		void onSendChatMessage(String message);
+	}
+
+	@FunctionalInterface
+	public interface Command {
+		/**
+		 * Called when the client sends a command,
+		 * which is whenever the player executes a command
+		 * including client commands registered with {@code fabric-command-api}.
+		 * Is not called when {@linkplain #ALLOW_COMMAND command messages are blocked}.
+		 * The command string does not include a slash at the beginning.
+		 *
+		 * @param command the command that will be sent to the server, without a slash at the beginning.
+		 */
+		void onSendCommandMessage(String command);
+	}
+
+	@FunctionalInterface
+	public interface ChatCanceled {
+		/**
+		 * Called when sending a chat message is canceled with {@link #ALLOW_CHAT}.
+		 *
+		 * @param message the message that is canceled from being sent to the server
+		 */
+		void onSendChatMessageCanceled(String message);
+	}
+
+	@FunctionalInterface
+	public interface CommandCanceled {
+		/**
+		 * Called when sending a command is canceled with {@link #ALLOW_COMMAND}.
+		 * The command string does not include a slash at the beginning.
+		 *
+		 * @param command the command that is being sent to the server, without a slash at the beginning.
+		 */
+		void onSendCommandMessageCanceled(String command);
+	}
+}
diff --git a/fabric-message-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/message/ClientPlayNetworkHandlerMixin.java b/fabric-message-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/message/ClientPlayNetworkHandlerMixin.java
new file mode 100644
index 000000000..864075004
--- /dev/null
+++ b/fabric-message-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/message/ClientPlayNetworkHandlerMixin.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.mixin.client.message;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.ModifyVariable;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+import net.minecraft.client.network.ClientPlayNetworkHandler;
+
+import net.fabricmc.fabric.api.client.message.v1.ClientSendMessageEvents;
+
+/**
+ * Mixin to {@link ClientPlayNetworkHandler} to listen for sending messages and commands.
+ * Priority set to 800 to inject before {@code fabric-command-api} so that this api will be called first.
+ */
+@Mixin(value = ClientPlayNetworkHandler.class, priority = 800)
+public abstract class ClientPlayNetworkHandlerMixin {
+	@Inject(method = "sendChatMessage", at = @At("HEAD"), cancellable = true)
+	private void fabric_allowSendChatMessage(String content, CallbackInfo ci) {
+		if (!ClientSendMessageEvents.ALLOW_CHAT.invoker().allowSendChatMessage(content)) {
+			ClientSendMessageEvents.CHAT_CANCELED.invoker().onSendChatMessageCanceled(content);
+			ci.cancel();
+		}
+	}
+
+	@ModifyVariable(method = "sendChatMessage", at = @At(value = "LOAD", ordinal = 0), ordinal = 0, argsOnly = true)
+	private String fabric_modifySendChatMessage(String content) {
+		content = ClientSendMessageEvents.MODIFY_CHAT.invoker().modifySendChatMessage(content);
+		ClientSendMessageEvents.CHAT.invoker().onSendChatMessage(content);
+		return content;
+	}
+
+	@Inject(method = "sendChatCommand", at = @At("HEAD"), cancellable = true)
+	private void fabric_allowSendCommandMessage(String command, CallbackInfo ci) {
+		if (!ClientSendMessageEvents.ALLOW_COMMAND.invoker().allowSendCommandMessage(command)) {
+			ClientSendMessageEvents.COMMAND_CANCELED.invoker().onSendCommandMessageCanceled(command);
+			ci.cancel();
+		}
+	}
+
+	@ModifyVariable(method = "sendChatCommand", at = @At(value = "LOAD", ordinal = 0), ordinal = 0, argsOnly = true)
+	private String fabric_modifySendCommandMessage(String command) {
+		command = ClientSendMessageEvents.MODIFY_COMMAND.invoker().modifySendCommandMessage(command);
+		ClientSendMessageEvents.COMMAND.invoker().onSendCommandMessage(command);
+		return command;
+	}
+}
diff --git a/fabric-message-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/message/MessageHandlerMixin.java b/fabric-message-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/message/MessageHandlerMixin.java
new file mode 100644
index 000000000..c4a5d8b32
--- /dev/null
+++ b/fabric-message-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/message/MessageHandlerMixin.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.mixin.client.message;
+
+import java.time.Instant;
+
+import com.mojang.authlib.GameProfile;
+import org.jetbrains.annotations.Nullable;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Unique;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.ModifyVariable;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+import net.minecraft.client.network.message.MessageHandler;
+import net.minecraft.network.message.MessageType;
+import net.minecraft.network.message.SignedMessage;
+import net.minecraft.text.Text;
+
+import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
+
+@Mixin(MessageHandler.class)
+public abstract class MessageHandlerMixin {
+	@Inject(method = "processChatMessageInternal", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/hud/InGameHud;getChatHud()Lnet/minecraft/client/gui/hud/ChatHud;", ordinal = 0), cancellable = true)
+	private void fabric_onSignedChatMessage(MessageType.Parameters params, SignedMessage message, Text decorated, GameProfile sender, boolean onlyShowSecureChat, Instant receptionTimestamp, CallbackInfoReturnable<Boolean> cir) {
+		fabric_onChatMessage(decorated, message, sender, params, receptionTimestamp, cir);
+	}
+
+	@Inject(method = "processChatMessageInternal", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/hud/InGameHud;getChatHud()Lnet/minecraft/client/gui/hud/ChatHud;", ordinal = 1), cancellable = true)
+	private void fabric_onFilteredSignedChatMessage(MessageType.Parameters params, SignedMessage message, Text decorated, GameProfile sender, boolean onlyShowSecureChat, Instant receptionTimestamp, CallbackInfoReturnable<Boolean> cir) {
+		Text filtered = message.filterMask().getFilteredText(message.getSignedContent());
+
+		if (filtered != null) {
+			fabric_onChatMessage(params.applyChatDecoration(filtered), message, sender, params, receptionTimestamp, cir);
+		}
+	}
+
+	@Inject(method = "method_45745", at = @At("HEAD"), cancellable = true)
+	private void fabric_onProfilelessChatMessage(MessageType.Parameters params, Text content, Instant receptionTimestamp, CallbackInfoReturnable<Boolean> cir) {
+		fabric_onChatMessage(params.applyChatDecoration(content), null, null, params, receptionTimestamp, cir);
+	}
+
+	@Unique
+	private void fabric_onChatMessage(Text message, @Nullable SignedMessage signedMessage, @Nullable GameProfile sender, MessageType.Parameters params, Instant receptionTimestamp, CallbackInfoReturnable<Boolean> cir) {
+		if (ClientReceiveMessageEvents.ALLOW_CHAT.invoker().allowReceiveChatMessage(message, signedMessage, sender, params, receptionTimestamp)) {
+			ClientReceiveMessageEvents.CHAT.invoker().onReceiveChatMessage(message, signedMessage, sender, params, receptionTimestamp);
+		} else {
+			ClientReceiveMessageEvents.CHAT_CANCELED.invoker().onReceiveChatMessageCanceled(message, signedMessage, sender, params, receptionTimestamp);
+			cir.setReturnValue(false);
+		}
+	}
+
+	@Inject(method = "onGameMessage", at = @At("HEAD"), cancellable = true)
+	private void fabric_allowGameMessage(Text message, boolean overlay, CallbackInfo ci) {
+		if (!ClientReceiveMessageEvents.ALLOW_GAME.invoker().allowReceiveGameMessage(message, overlay)) {
+			ClientReceiveMessageEvents.GAME_CANCELED.invoker().onReceiveGameMessageCanceled(message, overlay);
+			ci.cancel();
+		}
+	}
+
+	@ModifyVariable(method = "onGameMessage", at = @At(value = "LOAD", ordinal = 0), ordinal = 0, argsOnly = true)
+	private Text fabric_modifyGameMessage(Text message, Text message1, boolean overlay) {
+		message = ClientReceiveMessageEvents.MODIFY_GAME.invoker().modifyReceivedGameMessage(message, overlay);
+		ClientReceiveMessageEvents.GAME.invoker().onReceiveGameMessage(message, overlay);
+		return message;
+	}
+}
diff --git a/fabric-message-api-v1/src/client/resources/fabric-message-api-v1.client.mixins.json b/fabric-message-api-v1/src/client/resources/fabric-message-api-v1.client.mixins.json
new file mode 100644
index 000000000..00088d171
--- /dev/null
+++ b/fabric-message-api-v1/src/client/resources/fabric-message-api-v1.client.mixins.json
@@ -0,0 +1,12 @@
+{
+  "required": true,
+  "package": "net.fabricmc.fabric.mixin.client.message",
+  "compatibilityLevel": "JAVA_17",
+  "client": [
+    "ClientPlayNetworkHandlerMixin",
+    "MessageHandlerMixin"
+  ],
+  "injectors": {
+    "defaultRequire": 1
+  }
+}
diff --git a/fabric-message-api-v1/src/main/resources/fabric.mod.json b/fabric-message-api-v1/src/main/resources/fabric.mod.json
index 2ac9e4d52..7a0e078cb 100644
--- a/fabric-message-api-v1/src/main/resources/fabric.mod.json
+++ b/fabric-message-api-v1/src/main/resources/fabric.mod.json
@@ -21,7 +21,11 @@
   },
   "description": "Adds message-related hooks.",
   "mixins": [
-    "fabric-message-api-v1.mixins.json"
+    "fabric-message-api-v1.mixins.json",
+    {
+      "config": "fabric-message-api-v1.client.mixins.json",
+      "environment": "client"
+    }
   ],
   "custom": {
     "fabric-api:module-lifecycle": "experimental"
diff --git a/fabric-message-api-v1/src/testmod/java/net/fabricmc/fabric/test/message/ChatTestClient.java b/fabric-message-api-v1/src/testmod/java/net/fabricmc/fabric/test/message/ChatTestClient.java
new file mode 100644
index 000000000..6fb9d9e8a
--- /dev/null
+++ b/fabric-message-api-v1/src/testmod/java/net/fabricmc/fabric/test/message/ChatTestClient.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.test.message;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.minecraft.text.Text;
+
+import net.fabricmc.api.ClientModInitializer;
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
+import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
+import net.fabricmc.fabric.api.client.message.v1.ClientSendMessageEvents;
+
+public class ChatTestClient implements ClientModInitializer {
+	private static final Logger LOGGER = LoggerFactory.getLogger(ChatTestClient.class);
+
+	@Override
+	public void onInitializeClient() {
+		//Register test client commands
+		ClientCommandRegistrationCallback.EVENT.register((dispatcher, dedicated) -> dispatcher.register(ClientCommandManager.literal("block").then(ClientCommandManager.literal("send").executes(context -> {
+			throw new AssertionError("This client command should be blocked!");
+		}))));
+		//Test client send message events
+		ClientSendMessageEvents.ALLOW_CHAT.register((message) -> {
+			if (message.contains("block send")) {
+				LOGGER.info("Blocked chat message: " + message);
+				return false;
+			}
+
+			return true;
+		});
+		ClientSendMessageEvents.MODIFY_CHAT.register((message) -> {
+			if (message.contains("modify send")) {
+				LOGGER.info("Modifying chat message: " + message);
+				return "sending modified chat message";
+			}
+
+			return message;
+		});
+		ClientSendMessageEvents.CHAT.register((message -> LOGGER.info("Sent chat message: " + message)));
+		ClientSendMessageEvents.CHAT_CANCELED.register((message) -> LOGGER.info("Canceled sending chat message: " + message));
+		//Test client send command events
+		ClientSendMessageEvents.ALLOW_COMMAND.register((command) -> {
+			if (command.contains("block send")) {
+				LOGGER.info("Blocked command message: " + command);
+				return false;
+			}
+
+			return true;
+		});
+		ClientSendMessageEvents.MODIFY_COMMAND.register((command) -> {
+			if (command.contains("modify send")) {
+				LOGGER.info("Modifying command message: " + command);
+				return "sending modified command message";
+			}
+
+			return command;
+		});
+		ClientSendMessageEvents.COMMAND.register((command -> LOGGER.info("Sent command message: " + command)));
+		ClientSendMessageEvents.COMMAND_CANCELED.register((command) -> LOGGER.info("Canceled sending command message: " + command));
+		//Test client receive message events
+		ClientReceiveMessageEvents.ALLOW_CHAT.register((message, signedMessage, sender, params, receptionTimestamp) -> {
+			if (message.getString().contains("block receive")) {
+				LOGGER.info("Blocked receiving chat message: " + message.getString());
+				return false;
+			}
+
+			return true;
+		});
+		ClientReceiveMessageEvents.CHAT.register((message, signedMessage, sender, params, receptionTimestamp) -> LOGGER.info("Received chat message sent by {} at time {}: {}", sender == null ? "null" : sender.getName(), receptionTimestamp.toEpochMilli(), message.getString()));
+		ClientReceiveMessageEvents.CHAT_CANCELED.register((message, signedMessage, sender, params, receptionTimestamp) -> LOGGER.info("Cancelled receiving chat message sent by {} at time {}: {}", sender == null ? "null" : sender.getName(), receptionTimestamp.toEpochMilli(), message.getString()));
+		//Test client receive game message events
+		ClientReceiveMessageEvents.ALLOW_GAME.register((message, overlay) -> {
+			if (message.getString().contains("block receive")) {
+				LOGGER.info("Blocked receiving game message: " + message.getString());
+				return false;
+			}
+
+			return true;
+		});
+		ClientReceiveMessageEvents.MODIFY_GAME.register((message, overlay) -> {
+			if (message.getString().contains("modify receive")) {
+				LOGGER.info("Modifying received game message: " + message.getString());
+				return Text.of("modified receiving game message");
+			}
+
+			return message;
+		});
+		ClientReceiveMessageEvents.GAME.register((message, overlay) -> LOGGER.info("Received game message with overlay {}: {}", overlay, message.getString()));
+		ClientReceiveMessageEvents.GAME_CANCELED.register((message, overlay) -> LOGGER.info("Cancelled receiving game message with overlay {}: {}", overlay, message.getString()));
+	}
+}
diff --git a/fabric-message-api-v1/src/testmod/resources/fabric.mod.json b/fabric-message-api-v1/src/testmod/resources/fabric.mod.json
index 4a5a8e645..06bbd4d47 100644
--- a/fabric-message-api-v1/src/testmod/resources/fabric.mod.json
+++ b/fabric-message-api-v1/src/testmod/resources/fabric.mod.json
@@ -6,11 +6,15 @@
   "environment": "*",
   "license": "Apache-2.0",
   "depends": {
-    "fabric-message-api-v1": "*"
+    "fabric-message-api-v1": "*",
+    "fabric-command-api-v2": "*"
   },
   "entrypoints": {
     "main": [
       "net.fabricmc.fabric.test.message.ChatTest"
+    ],
+    "client": [
+      "net.fabricmc.fabric.test.message.ChatTestClient"
     ]
   }
 }