|
| 1 | +--- |
| 2 | +title: Signed messages |
| 3 | +description: A guide to working with SignedMessage objects. |
| 4 | +slug: paper/dev/component-api/signed-messages |
| 5 | +version: 1.19.4+ |
| 6 | +--- |
| 7 | + |
| 8 | +Since [Minecraft version 1.19](https://minecraft.wiki/w/Java_Edition_1.19), the client now signs any |
| 9 | +messages it sends so that they are uniquely identifiable and verifiable to be sent by a specific player. |
| 10 | +With this update, they also introduced the ability **delete specific messages** previously sent by a player. |
| 11 | + |
| 12 | +:::tip[Note] |
| 13 | + |
| 14 | +This guide does not go in-depth into the specifics of chat signing and its implementation on either client or server. |
| 15 | +For a full overview, you can refer [to this linked gist](https://gist.github.com/kennytv/ed783dd244ca0321bbd882c347892874). |
| 16 | + |
| 17 | +::: |
| 18 | + |
| 19 | +## How are signed messages represented in code? |
| 20 | +Paper uses Adventure's [`SignedMessage`](https://jd.advntr.dev/api/latest/net/kyori/adventure/chat/SignedMessage.html) |
| 21 | +object to represent a signed message. We differentiate two kinds of signed messages: system messages and non-system messages. |
| 22 | +System messages (checked with [`SignedMessage#isSystem()`](https://jd.advntr.dev/api/latest/net/kyori/adventure/chat/SignedMessage.html#isSystem())) |
| 23 | +are messages send by the server, whilst non-system messages are not. |
| 24 | + |
| 25 | +You can also differentiate the **signed plain text** `String` content of the message |
| 26 | +([`SignedMessage#message()`](https://jd.advntr.dev/api/latest/net/kyori/adventure/chat/SignedMessage.html#message())) |
| 27 | +from the unsigned, nullable [`Component`](https://jd.advntr.dev/api/latest/net/kyori/adventure/text/Component.html) |
| 28 | +content ([`SignedMessage#unsignedContent()`](https://jd.advntr.dev/api/latest/net/kyori/adventure/chat/SignedMessage.html#unsignedContent())). |
| 29 | + |
| 30 | +## Obtaining a signed message |
| 31 | +Signed messages can be obtained in two ways. |
| 32 | + |
| 33 | +1. From an [`AsyncChatEvent`](jd:paper:io.papermc.paper.event.player.AsyncChatEvent) using |
| 34 | + [`AbstractChatEvent#signedMessage()`](jd:paper:io.papermc.paper.event.player.AbstractChatEvent#signedMessage()). |
| 35 | + |
| 36 | +2. From an [`ArgumentTypes.signedMessage()`](jd:paper:io.papermc.paper.command.brigadier.argument.ArgumentTypes#signedMessage()) |
| 37 | + Brigadier argument type. |
| 38 | + |
| 39 | +## Using signed messages |
| 40 | +You can send signed message objects to an [`Audience`](https://jd.advntr.dev/api/latest/net/kyori/adventure/audience/Audience.html) |
| 41 | +using the [`Audience#sendMessage(SignedMessage, ChatType.Bound)`](https://jd.advntr.dev/api/latest/net/kyori/adventure/audience/Audience.html#sendMessage(net.kyori.adventure.chat.SignedMessage,net.kyori.adventure.chat.ChatType.Bound)) |
| 42 | +method. You can obtain a [`ChatType.Bound`](https://jd.advntr.dev/api/latest/net/kyori/adventure/chat/ChatType.Bound.html) object |
| 43 | +from the [`ChatType`](https://jd.advntr.dev/api/latest/net/kyori/adventure/chat/ChatType.html) interface. |
| 44 | + |
| 45 | +Deleting messages is much simpler. Adventure provides the [`Audience#deleteMessage(SignedMessage)`](https://jd.advntr.dev/api/latest/net/kyori/adventure/audience/Audience.html#deleteMessage(net.kyori.adventure.chat.SignedMessage)) |
| 46 | +or [`Audience#deleteMessage(SignedMessage.Signature)`](https://jd.advntr.dev/api/latest/net/kyori/adventure/audience/Audience.html#deleteMessage(net.kyori.adventure.chat.SignedMessage.Signature)) |
| 47 | +methods for that. |
| 48 | + |
| 49 | +## Example: Making user sent messages deletable |
| 50 | +For our example, we will create a chat format plugin which allows a user to delete |
| 51 | +their own messages in case they made a mistake. For this we will use the [`AsyncChatEvent`](jd:paper:io.papermc.paper.event.player.AsyncChatEvent). |
| 52 | + |
| 53 | +:::tip[AsyncChatEvent] |
| 54 | + |
| 55 | +The [`AsyncChatEvent`](jd:paper:io.papermc.paper.event.player.AsyncChatEvent) is covered in the [chat events](/paper/dev/chat-events) |
| 56 | +documentation page. If you want to read up on more detail on the chat renderer, you can do so there. |
| 57 | + |
| 58 | +::: |
| 59 | + |
| 60 | +### In-game preview |
| 61 | + |
| 62 | + |
| 63 | +### Code |
| 64 | +```java title="SignedChatListener.java" collapse={1-10} showLineNumbers |
| 65 | +package io.papermc.docs.signedmessages; |
| 66 | + |
| 67 | +import io.papermc.paper.event.player.AsyncChatEvent; |
| 68 | +import net.kyori.adventure.text.Component; |
| 69 | +import net.kyori.adventure.text.event.ClickEvent; |
| 70 | +import net.kyori.adventure.text.format.NamedTextColor; |
| 71 | +import net.kyori.adventure.text.format.TextDecoration; |
| 72 | +import org.bukkit.Bukkit; |
| 73 | +import org.bukkit.event.EventHandler; |
| 74 | +import org.bukkit.event.Listener; |
| 75 | + |
| 76 | +public class SignedChatListener implements Listener { |
| 77 | + |
| 78 | + @EventHandler |
| 79 | + void onPlayerChat(AsyncChatEvent event) { |
| 80 | + // We modify the chat format, so we use a chat renderer. |
| 81 | + event.renderer((player, playerName, message, viewer) -> { |
| 82 | + // This is the base format of our message. It will format chat as "<player> » <message>". |
| 83 | + final Component base = Component.textOfChildren( |
| 84 | + playerName.colorIfAbsent(NamedTextColor.GOLD), |
| 85 | + Component.text(" » ", NamedTextColor.DARK_GRAY), |
| 86 | + message |
| 87 | + ); |
| 88 | + |
| 89 | + // Send the base format to any player who is not the sender. |
| 90 | + if (viewer != player) { |
| 91 | + return base; |
| 92 | + } |
| 93 | + |
| 94 | + // Create a base delete suffix. The creation is separated into two |
| 95 | + // parts purely for readability reasons. |
| 96 | + final Component deleteCrossBase = Component.textOfChildren( |
| 97 | + Component.text("[", NamedTextColor.DARK_GRAY), |
| 98 | + Component.text("X", NamedTextColor.DARK_RED, TextDecoration.BOLD), |
| 99 | + Component.text("]", NamedTextColor.DARK_GRAY) |
| 100 | + ); |
| 101 | + |
| 102 | + // Add a hover and click event to the delete suffix. |
| 103 | + final Component deleteCross = deleteCrossBase |
| 104 | + .hoverEvent(Component.text("Click to delete your message!", NamedTextColor.RED)) |
| 105 | + // We retrieve the signed message with event.signedMessage() and request a server-wide deletion if the |
| 106 | + // deletion cross were to be clicked. |
| 107 | + .clickEvent(ClickEvent.callback(audience -> Bukkit.getServer().deleteMessage(event.signedMessage()))); |
| 108 | + |
| 109 | + // Send the base format but with the delete suffix. |
| 110 | + return base.appendSpace().append(deleteCross); |
| 111 | + }); |
| 112 | + } |
| 113 | +} |
| 114 | +``` |
0 commit comments