Skip to content

Commit

Permalink
Fix deserialization of Heartbeat events
Browse files Browse the repository at this point in the history
The d field can be null for Heartbeat send and receive events, see [1]
and [2].

This issue was reported on the Kord Discord server [3].

[1] https://discord.com/developers/docs/topics/gateway#heartbeat-requests
[2] https://discord.com/developers/docs/topics/gateway-events#heartbeat
[3] https://discord.com/channels/556525343595298817/1259582474262937732
  • Loading branch information
lukellmann committed Jul 8, 2024
1 parent 787893d commit 7cbd0d4
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 12 deletions.
13 changes: 9 additions & 4 deletions gateway/api/gateway.api
Original file line number Diff line number Diff line change
Expand Up @@ -1011,12 +1011,17 @@ public final class dev/kord/gateway/GuildUpdate : dev/kord/gateway/DispatchEvent
public final class dev/kord/gateway/Heartbeat : dev/kord/gateway/Event {
public static final field Companion Ldev/kord/gateway/Heartbeat$Companion;
public static final field NewCompanion Ldev/kord/gateway/Heartbeat$NewCompanion;
public fun <init> (J)V
public final fun component1 ()J
public final fun copy (J)Ldev/kord/gateway/Heartbeat;
public synthetic fun <init> (J)V
public fun <init> (Ljava/lang/Long;)V
public final synthetic fun component1 ()J
public final fun component1 ()Ljava/lang/Long;
public final synthetic fun copy (J)Ldev/kord/gateway/Heartbeat;
public final fun copy (Ljava/lang/Long;)Ldev/kord/gateway/Heartbeat;
public static synthetic fun copy$default (Ldev/kord/gateway/Heartbeat;JILjava/lang/Object;)Ldev/kord/gateway/Heartbeat;
public static synthetic fun copy$default (Ldev/kord/gateway/Heartbeat;Ljava/lang/Long;ILjava/lang/Object;)Ldev/kord/gateway/Heartbeat;
public fun equals (Ljava/lang/Object;)Z
public final fun getData ()J
public final synthetic fun getData ()J
public final fun getData ()Ljava/lang/Long;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
Expand Down
13 changes: 9 additions & 4 deletions gateway/api/gateway.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -1237,12 +1237,17 @@ final class dev.kord.gateway/GuildUpdate : dev.kord.gateway/DispatchEvent { // d

final class dev.kord.gateway/Heartbeat : dev.kord.gateway/Event { // dev.kord.gateway/Heartbeat|null[0]
constructor <init>(kotlin/Long) // dev.kord.gateway/Heartbeat.<init>|<init>(kotlin.Long){}[0]
constructor <init>(kotlin/Long?) // dev.kord.gateway/Heartbeat.<init>|<init>(kotlin.Long?){}[0]

final val data // dev.kord.gateway/Heartbeat.data|{}data[0]
final fun <get-data>(): kotlin/Long // dev.kord.gateway/Heartbeat.data.<get-data>|<get-data>(){}[0]

final fun component1(): kotlin/Long // dev.kord.gateway/Heartbeat.component1|component1(){}[0]
final fun copy(kotlin/Long = ...): dev.kord.gateway/Heartbeat // dev.kord.gateway/Heartbeat.copy|copy(kotlin.Long){}[0]
final fun <get-data>(): kotlin/Long? // dev.kord.gateway/Heartbeat.data.<get-data>|<get-data>(){}[0]
final val data_ // dev.kord.gateway/Heartbeat.data_|{}data_[0]
final fun <get-data_>(): kotlin/Long // dev.kord.gateway/Heartbeat.data_.<get-data_>|<get-data_>(){}[0]

final fun component1(): kotlin/Long? // dev.kord.gateway/Heartbeat.component1|component1(){}[0]
final fun component1_(): kotlin/Long // dev.kord.gateway/Heartbeat.component1_|component1_(){}[0]
final fun copy(kotlin/Long? = ...): dev.kord.gateway/Heartbeat // dev.kord.gateway/Heartbeat.copy|copy(kotlin.Long?){}[0]
final fun copy_(kotlin/Long = ...): dev.kord.gateway/Heartbeat // dev.kord.gateway/Heartbeat.copy_|copy_(kotlin.Long){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // dev.kord.gateway/Heartbeat.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // dev.kord.gateway/Heartbeat.hashCode|hashCode(){}[0]
final fun toString(): kotlin/String // dev.kord.gateway/Heartbeat.toString|toString(){}[0]
Expand Down
37 changes: 33 additions & 4 deletions gateway/src/commonMain/kotlin/Event.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import dev.kord.common.serialization.DurationInSeconds
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.datetime.Instant
import kotlinx.serialization.*
import kotlinx.serialization.builtins.nullable
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.CompositeDecoder
Expand All @@ -16,6 +17,7 @@ import kotlinx.serialization.encoding.decodeStructure
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonElement
import kotlin.jvm.JvmField
import kotlin.jvm.JvmName
import kotlinx.serialization.DeserializationStrategy as KDeserializationStrategy

private val jsonLogger = KotlinLogging.logger { }
Expand Down Expand Up @@ -288,13 +290,40 @@ public data class ReadyData(
)

@Serializable(with = Heartbeat.Serializer::class)
public data class Heartbeat(val data: Long) : Event() {
public data class Heartbeat(val data: Long?) : Event() {
internal object Serializer : KSerializer<Heartbeat> {
override val descriptor = PrimitiveSerialDescriptor("dev.kord.gateway.Heartbeat", PrimitiveKind.LONG)
override fun serialize(encoder: Encoder, value: Heartbeat) = encoder.encodeLong(value.data)
override fun deserialize(decoder: Decoder) = Heartbeat(decoder.decodeLong())
private val delegate = Long.serializer().nullable

override val descriptor = PrimitiveSerialDescriptor("dev.kord.gateway.Heartbeat", PrimitiveKind.LONG).nullable

override fun serialize(encoder: Encoder, value: Heartbeat) =
encoder.encodeSerializableValue(delegate, value.data)

override fun deserialize(decoder: Decoder) = Heartbeat(decoder.decodeSerializableValue(delegate))
}

@Deprecated("Binary compatibility, keep for some releases.", level = DeprecationLevel.HIDDEN)
public constructor(data: Long) : this(data as Long?)

@Suppress("PropertyName")
@Deprecated("Binary compatibility, keep for some releases.", level = DeprecationLevel.HIDDEN)
@get:JvmName("getData")
public val data_: Long
get() = data ?: throw NullPointerException("This heartbeat request contains a null sequence number")

@Suppress("FunctionName")
@Deprecated("Binary compatibility, keep for some releases.", level = DeprecationLevel.HIDDEN)
@JvmName("component1")
public fun component1_(): Long =
component1() ?: throw NullPointerException("This heartbeat request contains a null sequence number")

@Suppress("FunctionName")
@Deprecated("Binary compatibility, keep for some releases.", level = DeprecationLevel.HIDDEN)
@JvmName("copy")
public fun copy_(
data: Long = this.data ?: throw NullPointerException("This heartbeat request contains a null sequence number"),
): Heartbeat = Heartbeat(data as Long?)

public companion object {
@Suppress("DEPRECATION_ERROR")
@Deprecated(
Expand Down
18 changes: 18 additions & 0 deletions gateway/src/commonTest/kotlin/json/SerializationTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import kotlinx.serialization.MissingFieldException
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNull
import kotlin.js.JsName
import kotlin.random.Random
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
Expand Down Expand Up @@ -167,6 +168,23 @@ class SerializationTest {
}
}

@Test
fun test_Heartbeat_Event_serialization() {
val sequenceNumbers = listOf(null, 0, -1, 1, Random.nextLong(), Long.MIN_VALUE, Long.MAX_VALUE)
sequenceNumbers.forEach { sequenceNumber ->
val heartbeat = Heartbeat(sequenceNumber)
val permutations = listOf(
jsonObjectPermutations("op" to "1", "d" to "$sequenceNumber"),
jsonObjectPermutations("op" to "1", "t" to "null", "d" to "$sequenceNumber"),
jsonObjectPermutations("op" to "1", "s" to "null", "d" to "$sequenceNumber"),
jsonObjectPermutations("op" to "1", "t" to "null", "s" to "null", "d" to "$sequenceNumber"),
).flatten()
permutations.forEach { perm ->
assertEquals(heartbeat, Json.decodeFromString(Event.DeserializationStrategy, perm))
}
}
}

@Test
fun field_order_doesnt_matter() {
val data = listOf(
Expand Down

0 comments on commit 7cbd0d4

Please sign in to comment.