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
AsyncChatEvent
usingAbstractChatEvent#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); }); }}