Skip to content

Commit

Permalink
Rewrite VoiceEvent.DeserializationStrategy (#925)
Browse files Browse the repository at this point in the history
* Rewrite VoiceEvent.DeserializationStrategy

Similar to #923 but for the voice gateway.

* Log and ignore all unexpected opcodes
  • Loading branch information
lukellmann authored Mar 19, 2024
1 parent b3efeaf commit 76c5474
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 71 deletions.
16 changes: 5 additions & 11 deletions voice/api/voice.api
Original file line number Diff line number Diff line change
Expand Up @@ -577,17 +577,6 @@ public final class dev/kord/voice/gateway/HeartbeatAck : dev/kord/voice/gateway/
public fun toString ()Ljava/lang/String;
}

public final class dev/kord/voice/gateway/HeartbeatAck$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Ldev/kord/voice/gateway/HeartbeatAck$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ldev/kord/voice/gateway/HeartbeatAck;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ldev/kord/voice/gateway/HeartbeatAck;)V
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}

public final class dev/kord/voice/gateway/HeartbeatAck$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
Expand Down Expand Up @@ -656,6 +645,7 @@ public final class dev/kord/voice/gateway/Identify$Companion {

public final class dev/kord/voice/gateway/OpCode : java/lang/Enum {
public static final field ClientDisconnect Ldev/kord/voice/gateway/OpCode;
public static final field Companion Ldev/kord/voice/gateway/OpCode$Companion;
public static final field Heartbeat Ldev/kord/voice/gateway/OpCode;
public static final field HeartbeatAck Ldev/kord/voice/gateway/OpCode;
public static final field Hello Ldev/kord/voice/gateway/OpCode;
Expand All @@ -673,6 +663,10 @@ public final class dev/kord/voice/gateway/OpCode : java/lang/Enum {
public static fun values ()[Ldev/kord/voice/gateway/OpCode;
}

public final class dev/kord/voice/gateway/OpCode$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public final class dev/kord/voice/gateway/Ready : dev/kord/voice/gateway/VoiceEvent {
public static final field Companion Ldev/kord/voice/gateway/Ready$Companion;
public synthetic fun <init> (ILjava/lang/String;ILjava/util/List;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
Expand Down
12 changes: 6 additions & 6 deletions voice/src/main/kotlin/gateway/Command.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import kotlinx.serialization.SerializationStrategy as KSerializationStrategy
public sealed class Command {
public object SerializationStrategy : KSerializationStrategy<Command> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Command") {
element("op", OpCode.Serializer.descriptor)
element("op", OpCode.serializer().descriptor)
element("d", JsonObject.serializer().descriptor)
}

Expand All @@ -24,23 +24,23 @@ public sealed class Command {

when (value) {
is Identify -> {
composite.encodeSerializableElement(descriptor, 0, OpCode.Serializer, OpCode.Identify)
composite.encodeSerializableElement(descriptor, 0, OpCode.serializer(), OpCode.Identify)
composite.encodeSerializableElement(descriptor, 1, Identify.serializer(), value)
}
is Heartbeat -> {
composite.encodeSerializableElement(descriptor, 0, OpCode.Serializer, OpCode.Heartbeat)
composite.encodeSerializableElement(descriptor, 0, OpCode.serializer(), OpCode.Heartbeat)
composite.encodeLongElement(descriptor, 1, value.nonce)
}
is SendSpeaking -> {
composite.encodeSerializableElement(descriptor, 0, OpCode.Serializer, OpCode.Speaking)
composite.encodeSerializableElement(descriptor, 0, OpCode.serializer(), OpCode.Speaking)
composite.encodeSerializableElement(descriptor, 1, SendSpeaking.serializer(), value)
}
is SelectProtocol -> {
composite.encodeSerializableElement(descriptor, 0, OpCode.Serializer, OpCode.SelectProtocol)
composite.encodeSerializableElement(descriptor, 0, OpCode.serializer(), OpCode.SelectProtocol)
composite.encodeSerializableElement(descriptor, 1, SelectProtocol.serializer(), value)
}
is Resume -> {
composite.encodeSerializableElement(descriptor, 0, OpCode.Serializer, OpCode.Resume)
composite.encodeSerializableElement(descriptor, 0, OpCode.serializer(), OpCode.Resume)
composite.encodeSerializableElement(descriptor, 1, Resume.serializer(), value)
}
}
Expand Down
6 changes: 3 additions & 3 deletions voice/src/main/kotlin/gateway/OpCode.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package dev.kord.voice.gateway

import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

@Serializable(with = OpCode.Serializer::class)
public enum class OpCode(public val code: Int) {
Unknown(Int.MIN_VALUE),
Identify(0),
Expand All @@ -22,8 +23,7 @@ public enum class OpCode(public val code: Int) {
ClientDisconnect(13);

internal object Serializer : KSerializer<OpCode> {
override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor("op", PrimitiveKind.INT)
override val descriptor = PrimitiveSerialDescriptor("dev.kord.voice.gateway.OpCode", PrimitiveKind.INT)

private val entriesByCode = entries.associateBy { it.code }
override fun deserialize(decoder: Decoder): OpCode {
Expand Down
124 changes: 73 additions & 51 deletions voice/src/main/kotlin/gateway/VoiceEvent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,70 +4,71 @@ import dev.kord.common.entity.Snowflake
import dev.kord.voice.EncryptionMode
import dev.kord.voice.SpeakingFlags
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.nullable
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.encoding.decodeStructure
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.DeserializationStrategy as KDeserializationStrategy

private val jsonLogger = KotlinLogging.logger { }

public sealed class VoiceEvent {
public object DeserializationStrategy : KDeserializationStrategy<VoiceEvent?> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Event") {
element("op", OpCode.Serializer.descriptor)
element("d", JsonElement.serializer().descriptor)
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("dev.kord.voice.gateway.Event") {
element("op", OpCode.serializer().descriptor)
element("d", JsonElement.serializer().descriptor, isOptional = true)
}

@OptIn(ExperimentalSerializationApi::class)
override fun deserialize(decoder: Decoder): VoiceEvent? {
override fun deserialize(decoder: Decoder): VoiceEvent? = decoder.decodeStructure(descriptor) {
var op: OpCode? = null
var data: VoiceEvent? = null

with(decoder.beginStructure(descriptor)) {
loop@ while (true) {
when (val index = decodeElementIndex(descriptor)) {
CompositeDecoder.DECODE_DONE -> break@loop
0 -> op = OpCode.Serializer.deserialize(decoder)
1 -> data = when (op) {
OpCode.Hello -> decodeSerializableElement(
descriptor,
index,
Hello.serializer()
)
OpCode.HeartbeatAck -> {
HeartbeatAck(decodeInlineElement(HeartbeatAck.serializer().descriptor, 0).decodeLong())
}
OpCode.Ready -> decodeSerializableElement(descriptor, index, Ready.serializer())
OpCode.SessionDescription -> decodeSerializableElement(
descriptor,
index,
SessionDescription.serializer()
)
OpCode.Speaking -> decodeSerializableElement(descriptor, index, Speaking.serializer())
OpCode.Resumed -> Resumed
else -> {
val element = decodeNullableSerializableElement(
descriptor,
index,
JsonElement.serializer().nullable
)

jsonLogger.debug { "Unknown event with Opcode $op : $element" }
null
}
}
}
var d: JsonElement? = null
while (true) {
when (val index = decodeElementIndex(descriptor)) {
0 -> op = decodeSerializableElement(descriptor, index, OpCode.serializer(), op)
1 -> d = decodeSerializableElement(descriptor, index, JsonElement.serializer(), d)
CompositeDecoder.DECODE_DONE -> break
else -> throw SerializationException("Unexpected index: $index")
}
}
when (op) {
null ->
throw @OptIn(ExperimentalSerializationApi::class) MissingFieldException("op", descriptor.serialName)
OpCode.Ready -> decodeEvent(decoder, op, Ready.serializer(), d)
OpCode.SessionDescription -> decodeEvent(decoder, op, SessionDescription.serializer(), d)
OpCode.Speaking -> decodeEvent(decoder, op, Speaking.serializer(), d)
OpCode.HeartbeatAck -> decodeEvent(decoder, op, HeartbeatAck.serializer(), d)
OpCode.Hello -> decodeEvent(decoder, op, Hello.serializer(), d)
OpCode.Resumed -> {
// ignore the d field, Resumed is supposed to have null here:
// https://discord.com/developers/docs/topics/voice-connections#resuming-voice-connection-example-resumed-payload
Resumed
}
OpCode.Identify, OpCode.SelectProtocol, OpCode.Heartbeat, OpCode.Resume, OpCode.ClientDisconnect,
OpCode.Unknown,
-> {
jsonLogger.debug { "Unknown voice gateway event with opcode $op : $d" }
null
}
endStructure(descriptor)
return data
}
}

private fun <T> decodeEvent(
decoder: Decoder,
op: OpCode,
deserializer: KDeserializationStrategy<T>,
d: JsonElement?,
): T {
requireNotNull(d) { "Voice gateway event is missing 'd' field for opcode $op" }
// this cast will always succeed, otherwise decoder couldn't have decoded d
return (decoder as JsonDecoder).json.decodeFromJsonElement(deserializer, d)
}
}
}

Expand All @@ -87,8 +88,14 @@ public data class Hello(
val heartbeatInterval: Double
) : VoiceEvent()

@Serializable
public data class HeartbeatAck(val nonce: Long) : VoiceEvent()
@Serializable(with = HeartbeatAck.Serializer::class)
public data class HeartbeatAck(val nonce: Long) : VoiceEvent() {
internal object Serializer : KSerializer<HeartbeatAck> {
override val descriptor = PrimitiveSerialDescriptor("dev.kord.voice.gateway.HeartbeatAck", PrimitiveKind.LONG)
override fun serialize(encoder: Encoder, value: HeartbeatAck) = encoder.encodeLong(value.nonce)
override fun deserialize(decoder: Decoder) = HeartbeatAck(nonce = decoder.decodeLong())
}
}

@Serializable
public data class SessionDescription(
Expand All @@ -105,8 +112,23 @@ public data class Speaking(
val speaking: SpeakingFlags
) : VoiceEvent()

@Serializable
public object Resumed : VoiceEvent()
public object Resumed : VoiceEvent() {
@Deprecated(
"'Resumed' is no longer serializable, deserialize it with 'VoiceEvent.DeserializationStrategy' instead. " +
"Deprecated without a replacement.",
level = DeprecationLevel.WARNING,
)
public fun serializer(): KSerializer<Resumed> = serializer

private val serializer: KSerializer<Resumed> by lazy(LazyThreadSafetyMode.PUBLICATION) {
@Suppress("INVISIBLE_MEMBER")
kotlinx.serialization.internal.ObjectSerializer(
serialName = "dev.kord.voice.gateway.Resumed",
objectInstance = Resumed,
classAnnotations = arrayOf(),
)
}
}

public sealed class Close : VoiceEvent() {
/**
Expand Down

0 comments on commit 76c5474

Please sign in to comment.