-
Notifications
You must be signed in to change notification settings - Fork 0
✨ feat(api): introduce new chat signature and message handling features #420
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
c9c16d5
✨ feat(api): introduce new chat signature and message handling features
TheBjoRedCraft b5c7437
✨ feat(player): rename SurfPlayer to SurfMinestomPlayer and update re…
TheBjoRedCraft bfa1b46
Merge branch 'version/26.2' into fix/remove-lobby-api
TheBjoRedCraft File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
4 changes: 2 additions & 2 deletions
4
surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/SurfApiMinestomPlugin.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
83 changes: 83 additions & 0 deletions
83
surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/chat/AsyncChatEvent.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| package dev.slne.surf.api.minestom.chat | ||
|
|
||
| import dev.slne.surf.api.minestom.event.SuspendingEventNode | ||
| import dev.slne.surf.api.minestom.player.SurfMinestomPlayer | ||
| import net.kyori.adventure.audience.Audience | ||
| import net.kyori.adventure.chat.SignedMessage | ||
| import net.kyori.adventure.text.Component | ||
| import org.jetbrains.annotations.ApiStatus | ||
|
|
||
| /** | ||
| * Fired when a [SurfMinestomPlayer] sends a chat message. | ||
| * | ||
| * Listeners run off the tick thread on | ||
| * a virtual thread, so they may suspend *and* they may block | ||
| * without affecting the server's tick rate. | ||
| * | ||
| * Messages from one player are processed strictly in order, so a slow listener delays that player's | ||
| * next message. | ||
| * | ||
| * ``` | ||
| * AsyncChatEvent.addListener { event -> | ||
| * val prefix = database.loadPrefix(event.player.uuid) // suspending or blocking, both fine | ||
| * event.renderer = ChatRenderer.viewerUnaware { _, name, message -> | ||
| * Component.text().append(prefix).append(name).append(Component.text(": ")).append(message).build() | ||
| * } | ||
| * } | ||
| * ``` | ||
| */ | ||
| class AsyncChatEvent @ApiStatus.Internal constructor( | ||
|
|
||
| val player: SurfMinestomPlayer, | ||
|
|
||
| /** | ||
| * The [Audience]s that this chat message will be displayed to. | ||
| * | ||
| * Can be modified to add and remove viewers. | ||
| */ | ||
| val viewers: MutableSet<Audience>, | ||
|
|
||
| /** | ||
| * The renderer used to turn [message] into the component every viewer sees. | ||
| */ | ||
| var renderer: ChatRenderer, | ||
|
|
||
| /** | ||
| * The user-supplied message. | ||
| */ | ||
| var message: Component, | ||
|
|
||
| /** | ||
| * The original and unmodified user-supplied message. | ||
| * | ||
| * The value will **not** reflect changes made through [message]. | ||
| */ | ||
| val originalMessage: Component, | ||
|
|
||
| /** | ||
| * The signed message backing this event. | ||
| * | ||
| * Changes made in this event will **not** update the signed message. | ||
| */ | ||
| val signedMessage: SignedMessage, | ||
| ) { | ||
|
|
||
| var isCancelled = false | ||
|
|
||
| companion object { | ||
| @ApiStatus.Internal | ||
| val node = SuspendingEventNode<AsyncChatEvent>("async-chat") | ||
|
|
||
| typealias AsyncChatEventListener = suspend (AsyncChatEvent) -> Unit | ||
|
|
||
| /** | ||
| * Registers a chat listener. Lower [priority] runs first. | ||
| * | ||
| * @see SuspendingEventNode.addListener | ||
| */ | ||
| fun addListener( | ||
| priority: Int = 0, | ||
| listener: AsyncChatEventListener | ||
| ) = node.addListener(priority, listener) | ||
| } | ||
| } |
74 changes: 74 additions & 0 deletions
74
surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/chat/ChatRenderer.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| package dev.slne.surf.api.minestom.chat | ||
|
|
||
| import net.kyori.adventure.audience.Audience | ||
| import net.kyori.adventure.text.Component | ||
| import net.minestom.server.entity.Player | ||
| import org.jetbrains.annotations.ApiStatus | ||
|
|
||
| /** | ||
| * A chat renderer is responsible for rendering chat messages sent by [Player]s to the server. | ||
| */ | ||
| fun interface ChatRenderer { | ||
|
|
||
| /** | ||
| * Renders a chat message. This will be called once for each receiving [Audience]. | ||
| * | ||
| * @param source the message source | ||
| * @param sourceDisplayName the display name of the source player | ||
| * @param message the chat message | ||
| * @param viewer the receiving [Audience] | ||
| * @return a rendered chat message | ||
| */ | ||
| @ApiStatus.OverrideOnly | ||
| suspend fun render( | ||
| source: Player, | ||
| sourceDisplayName: Component, | ||
| message: Component, | ||
| viewer: Audience | ||
| ): Component | ||
|
|
||
| /** | ||
| * Marker for the renderer returned by [defaultRenderer]. | ||
| */ | ||
| @ApiStatus.Internal | ||
| sealed interface Default : ChatRenderer, ViewerUnaware | ||
|
|
||
| /** | ||
| * Similar to [ChatRenderer], but without knowledge of the message viewer. | ||
| * | ||
| * @see viewerUnaware | ||
| */ | ||
| fun interface ViewerUnaware { | ||
|
|
||
| /** | ||
| * Renders a chat message. | ||
| * | ||
| * @param source the message source | ||
| * @param sourceDisplayName the display name of the source player | ||
| * @param message the chat message | ||
| * @return a rendered chat message | ||
| */ | ||
| @ApiStatus.OverrideOnly | ||
| suspend fun render(source: Player, sourceDisplayName: Component, message: Component): Component | ||
| } | ||
|
|
||
| companion object { | ||
|
|
||
| /** | ||
| * Creates a new instance of the default [ChatRenderer]. | ||
| */ | ||
| fun defaultRenderer(): ChatRenderer = | ||
| ViewerUnawareChatRenderer.Default { _, sourceDisplayName, message -> | ||
| Component.translatable("chat.type.text", sourceDisplayName, message) | ||
| } | ||
|
|
||
| /** | ||
| * Creates a new viewer-unaware [ChatRenderer], which will render the chat message a single | ||
| * time, displaying the same rendered message to every viewing [Audience]. | ||
| * | ||
| * @param renderer the viewer unaware renderer | ||
| */ | ||
| fun viewerUnaware(renderer: ViewerUnaware): ChatRenderer = | ||
| ViewerUnawareChatRenderer.Impl(renderer) | ||
| } | ||
| } |
16 changes: 16 additions & 0 deletions
16
surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/chat/RemoteChatSender.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package dev.slne.surf.api.minestom.chat | ||
|
|
||
| import net.minestom.server.crypto.ChatSession | ||
| import java.util.UUID | ||
|
|
||
| /** | ||
| * The player a [RemoteSignedMessage] came from, as far as the receiving server knows them. | ||
| * | ||
| * [session] is the chat session the message was signed under. It is `null` for a sender that had | ||
| * none, in which case the message is shown as unsigned. | ||
| */ | ||
| data class RemoteChatSender( | ||
| val uuid: UUID, | ||
| val username: String, | ||
| val session: ChatSession?, | ||
| ) |
37 changes: 37 additions & 0 deletions
37
surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/chat/RemoteSignedMessage.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| package dev.slne.surf.api.minestom.chat | ||
|
|
||
| import net.kyori.adventure.text.Component | ||
| import net.minestom.server.crypto.MessageSignature | ||
| import java.time.Instant | ||
| import java.util.* | ||
|
|
||
| /** | ||
| * A signed chat message in a form that survives leaving this server. | ||
| * | ||
| * It carries everything a client needs to verify the signature, so a server that never saw the | ||
| * sender can still show the message as signed. A message from a sender without a chat session has no | ||
| * [signature], a nil [sessionId] and is shown as unsigned. | ||
| * | ||
| * @see dev.slne.surf.api.minestom.player.SurfMinestomPlayer.captureSignedMessage | ||
| * @see dev.slne.surf.api.minestom.player.SurfMinestomPlayer.sendRemoteSignedMessage | ||
| */ | ||
| data class RemoteSignedMessage( | ||
| val sender: UUID, | ||
| val sessionId: UUID, | ||
|
|
||
| /** The position of the message in the signature chain of its session. */ | ||
| val index: Int, | ||
|
|
||
| val signature: MessageSignature?, | ||
|
|
||
| /** The message as it was signed, which is the plain text the sender typed. */ | ||
| val content: String, | ||
| val timestamp: Instant, | ||
| val salt: Long, | ||
|
|
||
| /** The signatures of the messages the sender had seen, which the signature also covers. */ | ||
| val lastSeen: List<MessageSignature>, | ||
|
|
||
| /** What the receiver sees instead of [content]. */ | ||
| val unsignedContent: Component?, | ||
| ) |
36 changes: 36 additions & 0 deletions
36
...api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/chat/ViewerUnawareChatRenderer.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| package dev.slne.surf.api.minestom.chat | ||
|
|
||
| import net.kyori.adventure.audience.Audience | ||
| import net.kyori.adventure.text.Component | ||
| import net.minestom.server.entity.Player | ||
|
|
||
| /** | ||
| * Adapts a [ChatRenderer.ViewerUnaware] to a full [ChatRenderer], rendering the message only once | ||
| * and reusing the result for every viewer. | ||
| */ | ||
| internal sealed class ViewerUnawareChatRenderer( | ||
| private val unaware: ChatRenderer.ViewerUnaware | ||
| ) : ChatRenderer, ChatRenderer.ViewerUnaware { | ||
|
|
||
| private var rendered: Component? = null | ||
|
|
||
| override suspend fun render( | ||
| source: Player, | ||
| sourceDisplayName: Component, | ||
| message: Component, | ||
| viewer: Audience | ||
| ): Component = render(source, sourceDisplayName, message) | ||
|
|
||
| override suspend fun render( | ||
| source: Player, | ||
| sourceDisplayName: Component, | ||
| message: Component | ||
| ): Component = rendered ?: unaware.render(source, sourceDisplayName, message).also { | ||
| rendered = it | ||
| } | ||
|
TheBjoRedCraft marked this conversation as resolved.
|
||
|
|
||
| class Impl(unaware: ChatRenderer.ViewerUnaware) : ViewerUnawareChatRenderer(unaware) | ||
|
|
||
| class Default(unaware: ChatRenderer.ViewerUnaware) : | ||
| ViewerUnawareChatRenderer(unaware), ChatRenderer.Default | ||
| } | ||
67 changes: 67 additions & 0 deletions
67
...pi-minestom/src/main/kotlin/dev/slne/surf/api/minestom/chat/signature/LastSeenMessages.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| package dev.slne.surf.api.minestom.chat.signature | ||
|
|
||
| import com.google.common.primitives.Ints | ||
| import dev.slne.surf.api.minestom.chat.signature.LastSeenMessages.Update.Companion.IGNORE_CHECKSUM | ||
| import net.minestom.server.crypto.MessageSignature | ||
| import net.minestom.server.network.packet.client.play.ClientChatMessagePacket | ||
| import net.minestom.server.network.packet.client.play.ClientSignedCommandChatPacket | ||
| import java.util.BitSet | ||
| import net.minestom.server.crypto.LastSeenMessages as NetworkLastSeenMessages | ||
|
|
||
| @JvmInline | ||
| value class LastSeenMessages(val entries: List<MessageSignature>) { | ||
|
|
||
| companion object { | ||
| const val LAST_SEEN_MESSAGES_MAX_LENGTH = 20 | ||
|
|
||
| val EMPTY = LastSeenMessages(emptyList()) | ||
| } | ||
|
|
||
| fun computeChecksum(): Byte { | ||
| var checksum = 1 | ||
|
|
||
| for (entry in entries) { | ||
| checksum = 31 * checksum + entry.checksum() | ||
| } | ||
|
|
||
| val checksumByte = checksum.toByte() | ||
|
|
||
| return if (checksumByte == IGNORE_CHECKSUM) 1 else checksumByte | ||
| } | ||
|
|
||
| fun updateSignature(output: SignatureUpdater.Output) { | ||
| output.update(Ints.toByteArray(entries.size)) | ||
|
|
||
| for (entry in entries) { | ||
| output.update(entry.signature()) | ||
| } | ||
| } | ||
|
|
||
| fun pack(cache: MessageSignatureCache): NetworkLastSeenMessages.Packed = | ||
| NetworkLastSeenMessages.Packed(entries.map { it.pack(cache) }) | ||
|
|
||
| data class Update( | ||
| val offset: Int, | ||
| val acknowledged: BitSet, | ||
| val checksum: Byte | ||
| ) { | ||
| companion object { | ||
| const val IGNORE_CHECKSUM = 0.toByte() | ||
|
|
||
| fun fromPacket(packet: ClientChatMessagePacket) = Update( | ||
| offset = packet.ackOffset(), | ||
| acknowledged = packet.ackList(), | ||
| checksum = packet.checksum() | ||
| ) | ||
|
|
||
| fun fromPacket(packet: ClientSignedCommandChatPacket) = Update( | ||
| offset = packet.lastSeenMessages().offset(), | ||
| acknowledged = packet.lastSeenMessages().acknowledged(), | ||
| checksum = packet.checksum() | ||
| ) | ||
| } | ||
|
|
||
| fun verifyChecksum(lastSeen: LastSeenMessages): Boolean = | ||
| checksum == IGNORE_CHECKSUM || checksum == lastSeen.computeChecksum() | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.