Signed messages
Since Minecraft version 1.19, the client now signs any messages it sends so that they are uniquely identifiable and verifiable to be sent by a specific player. With this update, they also introduced the ability delete specific messages previously sent by a player.
How are signed messages represented in code?
Section titled “How are signed messages represented in code?”Paper uses Adventure’s SignedMessage
object to represent a signed message. We differentiate two kinds of signed messages: system messages and non-system messages.
System messages (checked with SignedMessage#isSystem())
are messages send by the server, whilst non-system messages are not.
You can also differentiate the signed plain text String content of the message
(SignedMessage#message())
from the unsigned, nullable Component
content (SignedMessage#unsignedContent()).
Obtaining a signed message
Section titled “Obtaining a signed message”Signed messages can be obtained in two ways.
- 
From an
AsyncChatEventusingAbstractChatEvent#signedMessage(). - 
From an
ArgumentTypes.signedMessage()Brigadier argument type. 
Using signed messages
Section titled “Using signed messages”You can send signed message objects to an Audience
using the Audience#sendMessage(SignedMessage, ChatType.Bound)
method. You can obtain a ChatType.Bound object
from the ChatType interface.
Deleting messages is much simpler. Adventure provides the Audience#deleteMessage(SignedMessage)
or Audience#deleteMessage(SignedMessage.Signature)
methods for that.
Example: Making user sent messages deletable
Section titled “Example: Making user sent messages deletable”For our example, we will create a chat format plugin which allows a user to delete
their own messages in case they made a mistake. For this we will use the AsyncChatEvent.
In-game preview
Section titled “In-game preview”
10 collapsed lines
package io.papermc.docs.signedmessages;
import io.papermc.paper.event.player.AsyncChatEvent;import net.kyori.adventure.text.Component;import net.kyori.adventure.text.event.ClickEvent;import net.kyori.adventure.text.format.NamedTextColor;import net.kyori.adventure.text.format.TextDecoration;import org.bukkit.Bukkit;import org.bukkit.event.EventHandler;import org.bukkit.event.Listener;
public class SignedChatListener implements Listener {
    @EventHandler    void onPlayerChat(AsyncChatEvent event) {        // We modify the chat format, so we use a chat renderer.        event.renderer((player, playerName, message, viewer) -> {            // This is the base format of our message. It will format chat as "<player> » <message>".            final Component base = Component.textOfChildren(                playerName.colorIfAbsent(NamedTextColor.GOLD),                Component.text(" » ", NamedTextColor.DARK_GRAY),                message            );
            // Send the base format to any player who is not the sender.            if (viewer != player) {                return base;            }
            // Create a base delete suffix. The creation is separated into two            // parts purely for readability reasons.            final Component deleteCrossBase = Component.textOfChildren(                Component.text("[", NamedTextColor.DARK_GRAY),                Component.text("X", NamedTextColor.DARK_RED, TextDecoration.BOLD),                Component.text("]", NamedTextColor.DARK_GRAY)            );
            // Add a hover and click event to the delete suffix.            final Component deleteCross = deleteCrossBase                .hoverEvent(Component.text("Click to delete your message!", NamedTextColor.RED))                // We retrieve the signed message with event.signedMessage() and request a server-wide deletion if the                // deletion cross were to be clicked.                .clickEvent(ClickEvent.callback(audience -> Bukkit.getServer().deleteMessage(event.signedMessage())));
            // Send the base format but with the delete suffix.            return base.appendSpace().append(deleteCross);        });    }}