Skip to content

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()).

Signed messages can be obtained in two ways.

  1. From an AsyncChatEvent using AbstractChatEvent#signedMessage().

  2. From an ArgumentTypes.signedMessage() Brigadier argument type.

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.

SignedChatListener.java
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);
});
}
}