From 0859e4db8dd09338b973a5279590ccb08f3ed534 Mon Sep 17 00:00:00 2001 From: Lost Date: Sun, 17 Oct 2021 20:33:32 -0400 Subject: [PATCH] explicit api for voice --- voice/build.gradle.kts | 5 ++ voice/src/main/kotlin/AudioFrame.kt | 8 +-- voice/src/main/kotlin/AudioProvider.kt | 6 +- voice/src/main/kotlin/EncryptionMode.kt | 4 +- voice/src/main/kotlin/FrameInterceptor.kt | 14 ++--- voice/src/main/kotlin/SpeakingFlag.kt | 43 +++++++------- voice/src/main/kotlin/VoiceConnection.kt | 38 ++++++------ .../src/main/kotlin/VoiceConnectionBuilder.kt | 38 ++++++------ .../encryption/XSalsa20Poly1305Codec.kt | 32 ++++++++-- .../strategies/LiteNonceStrategy.kt | 2 +- .../encryption/strategies/NonceStrategy.kt | 10 ++-- .../strategies/NormalNonceStrategy.kt | 2 +- .../strategies/SuffixNonceStrategy.kt | 2 +- .../VoiceConnectionInitializationException.kt | 8 +-- voice/src/main/kotlin/gateway/Command.kt | 16 ++--- .../kotlin/gateway/DefaultVoiceGateway.kt | 8 +-- .../gateway/DefaultVoiceGatewayBuilder.kt | 16 ++--- voice/src/main/kotlin/gateway/OpCode.kt | 4 +- voice/src/main/kotlin/gateway/Ticker.kt | 3 +- voice/src/main/kotlin/gateway/VoiceEvent.kt | 30 +++++----- voice/src/main/kotlin/gateway/VoiceGateway.kt | 59 ++++++++++--------- .../gateway/VoiceGatewayConfiguration.kt | 2 +- voice/src/main/kotlin/io/ByteArrayCursors.kt | 56 +++++++++--------- voice/src/main/kotlin/io/ByteArrayView.kt | 30 +++++----- .../src/main/kotlin/streams/DefaultStreams.kt | 2 +- voice/src/main/kotlin/streams/NOPStreams.kt | 10 ++-- voice/src/main/kotlin/streams/Streams.kt | 12 ++-- voice/src/main/kotlin/udp/AudioFrameSender.kt | 6 +- .../main/kotlin/udp/AudioPacketProvider.kt | 6 +- .../kotlin/udp/DefaultAudioFrameSender.kt | 6 +- .../main/kotlin/udp/GlobalVoiceUdpSocket.kt | 5 +- voice/src/main/kotlin/udp/PayloadType.kt | 12 ++-- voice/src/main/kotlin/udp/RTPPacket.kt | 48 ++++++++------- voice/src/main/kotlin/udp/VoiceUdpSocket.kt | 17 +++--- 34 files changed, 293 insertions(+), 267 deletions(-) diff --git a/voice/build.gradle.kts b/voice/build.gradle.kts index 5b1c35726a52..e93e5757f8cd 100644 --- a/voice/build.gradle.kts +++ b/voice/build.gradle.kts @@ -14,6 +14,11 @@ dependencies { api(libs.ktor.network) } +// Move this into the build script plugins when implemented in all modules +kotlin { + explicitApi() +} + // by convention, java classes (TweetNaclFast) should be in their own java source. // however, this breaks atomicfu. // to work around it we just make the kotlin src directory also a java src directory. diff --git a/voice/src/main/kotlin/AudioFrame.kt b/voice/src/main/kotlin/AudioFrame.kt index 582b1b8a519b..dbb3d1041d71 100644 --- a/voice/src/main/kotlin/AudioFrame.kt +++ b/voice/src/main/kotlin/AudioFrame.kt @@ -7,10 +7,10 @@ import dev.kord.common.annotation.KordVoice */ @KordVoice @JvmInline -value class AudioFrame(val data: ByteArray) { - companion object { - val SILENCE = AudioFrame(byteArrayOf(0xFC.toByte(), 0xFF.toByte(), 0xFE.toByte())) +public value class AudioFrame(public val data: ByteArray) { + public companion object { + public val SILENCE: AudioFrame = AudioFrame(byteArrayOf(0xFC.toByte(), 0xFF.toByte(), 0xFE.toByte())) - fun fromData(data: ByteArray?) = data?.let(::AudioFrame) + public fun fromData(data: ByteArray?): AudioFrame? = data?.let(::AudioFrame) } } \ No newline at end of file diff --git a/voice/src/main/kotlin/AudioProvider.kt b/voice/src/main/kotlin/AudioProvider.kt index 0fae07a5f75f..ca081164488f 100644 --- a/voice/src/main/kotlin/AudioProvider.kt +++ b/voice/src/main/kotlin/AudioProvider.kt @@ -13,20 +13,20 @@ import kotlin.time.TimeSource * which should be transmitted to Discord. */ @KordVoice -fun interface AudioProvider { +public fun interface AudioProvider { /** * Provides a single frame of audio, [AudioFrame]. * * @return the frame of audio. */ - suspend fun provide(): AudioFrame? + public suspend fun provide(): AudioFrame? /** * Polls [AudioFrame]s into the [frames] channel at an appropriate interval. Suspends until the coroutine scope is cancelled. * * @param frames the channel where [AudioFrame]s will be sent to. */ - suspend fun CoroutineScope.provideFrames(frames: SendChannel) { + public suspend fun CoroutineScope.provideFrames(frames: SendChannel) { val mark = TimeSource.Monotonic.markNow() var nextFrameTimestamp = mark.elapsedNow().inWholeNanoseconds diff --git a/voice/src/main/kotlin/EncryptionMode.kt b/voice/src/main/kotlin/EncryptionMode.kt index b94df1ef950d..875c730dbc32 100644 --- a/voice/src/main/kotlin/EncryptionMode.kt +++ b/voice/src/main/kotlin/EncryptionMode.kt @@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable @KordVoice @Serializable -enum class EncryptionMode { +public enum class EncryptionMode { @SerialName("xsalsa20_poly1305") XSalsa20Poly1305, @@ -16,10 +16,10 @@ enum class EncryptionMode { @SerialName("xsalsa20_poly1305_lite") XSalsa20Poly1305Lite, + // video/unreleased-audio stuff... unused. though required to allow for serialization of ready @SerialName("xsalsa20_poly1305_lite_rtpsize") XSalsa20Poly1305LiteRtpsize, - // video/unreleased-audio stuff... unused. though required to allow for serialization of ready @SerialName("aead_aes256_gcm_rtpsize") AeadAes256GcmRtpsize, diff --git a/voice/src/main/kotlin/FrameInterceptor.kt b/voice/src/main/kotlin/FrameInterceptor.kt index 8ea0e801c888..8b5874345e48 100644 --- a/voice/src/main/kotlin/FrameInterceptor.kt +++ b/voice/src/main/kotlin/FrameInterceptor.kt @@ -14,17 +14,17 @@ import kotlin.properties.Delegates * @param ssrc the current SSRC retrieved from Discord. */ @KordVoice -data class FrameInterceptorContext( +public data class FrameInterceptorContext( val gateway: Gateway, val voiceGateway: VoiceGateway, val ssrc: UInt, ) @KordVoice -class FrameInterceptorContextBuilder(var gateway: Gateway, var voiceGateway: VoiceGateway) { - var ssrc: UInt by Delegates.notNull() +public class FrameInterceptorContextBuilder(public var gateway: Gateway, public var voiceGateway: VoiceGateway) { + public var ssrc: UInt by Delegates.notNull() - fun build() = FrameInterceptorContext(gateway, voiceGateway, ssrc) + public fun build(): FrameInterceptorContext = FrameInterceptorContext(gateway, voiceGateway, ssrc) } @KordVoice @@ -37,8 +37,8 @@ internal inline fun FrameInterceptorContext(gateway: Gateway, voiceGateway: Voic * @see DefaultFrameInterceptor */ @KordVoice -fun interface FrameInterceptor { - suspend fun intercept(frame: AudioFrame?): AudioFrame? +public fun interface FrameInterceptor { + public suspend fun intercept(frame: AudioFrame?): AudioFrame? } private const val FRAMES_OF_SILENCE_TO_PLAY = 5 @@ -52,7 +52,7 @@ private const val FRAMES_OF_SILENCE_TO_PLAY = 5 * @param speakingState the speaking state that will be used when there is audio data to be sent. By default, it is microphone-only. */ @KordVoice -open class DefaultFrameInterceptor( +public open class DefaultFrameInterceptor( protected val context: FrameInterceptorContext, private val speakingState: SpeakingFlags = SpeakingFlags { +SpeakingFlag.Microphone } ) : FrameInterceptor { diff --git a/voice/src/main/kotlin/SpeakingFlag.kt b/voice/src/main/kotlin/SpeakingFlag.kt index 85a029e46f3c..38298fa96e5f 100644 --- a/voice/src/main/kotlin/SpeakingFlag.kt +++ b/voice/src/main/kotlin/SpeakingFlag.kt @@ -13,7 +13,7 @@ import kotlin.contracts.InvocationKind import kotlin.contracts.contract @KordVoice -enum class SpeakingFlag(val code: Int) { +public enum class SpeakingFlag(public val code: Int) { Microphone(1 shl 0), Soundshare(1 shl 1), Priority(1 shl 2) @@ -21,23 +21,22 @@ enum class SpeakingFlag(val code: Int) { @KordVoice @Serializable(with = SpeakingFlags.Serializer::class) -class SpeakingFlags internal constructor(val code: Int) { - val flags = SpeakingFlag.values().filter { code and it.code != 0 } +public class SpeakingFlags internal constructor(public val code: Int) { + public val flags: List = SpeakingFlag.values().filter { code and it.code != 0 } - operator fun contains(flag: SpeakingFlags) = flag.code and this.code == flag.code + public operator fun contains(flag: SpeakingFlags): Boolean = flag.code and this.code == flag.code - operator fun contains(flags: SpeakingFlag) = flags.code and this.code == flags.code + public operator fun contains(flags: SpeakingFlag): Boolean = flags.code and this.code == flags.code - operator fun plus(flags: SpeakingFlags): SpeakingFlags = SpeakingFlags(this.code or flags.code) + public operator fun plus(flags: SpeakingFlags): SpeakingFlags = SpeakingFlags(this.code or flags.code) - operator fun plus(flags: SpeakingFlag): SpeakingFlags = SpeakingFlags(this.code or flags.code) + public operator fun plus(flags: SpeakingFlag): SpeakingFlags = SpeakingFlags(this.code or flags.code) - operator fun minus(flags: SpeakingFlags): SpeakingFlags = SpeakingFlags(this.code xor flags.code) + public operator fun minus(flags: SpeakingFlags): SpeakingFlags = SpeakingFlags(this.code xor flags.code) - operator fun minus(flags: SpeakingFlag): SpeakingFlags = SpeakingFlags(this.code xor flags.code) + public operator fun minus(flags: SpeakingFlag): SpeakingFlags = SpeakingFlags(this.code xor flags.code) - - inline fun copy(block: Builder.() -> Unit): SpeakingFlags { + public inline fun copy(block: Builder.() -> Unit): SpeakingFlags { val builder = Builder(code) builder.apply(block) return builder.flags() @@ -58,56 +57,56 @@ class SpeakingFlags internal constructor(val code: Int) { } } - class Builder(internal var code: Int = 0) { - operator fun SpeakingFlag.unaryPlus() { + public class Builder(internal var code: Int = 0) { + public operator fun SpeakingFlag.unaryPlus() { this@Builder.code = this@Builder.code or code } - operator fun SpeakingFlag.unaryMinus() { + public operator fun SpeakingFlag.unaryMinus() { if (this@Builder.code and code == code) { this@Builder.code = this@Builder.code xor code } } - operator fun SpeakingFlags.unaryPlus() { + public operator fun SpeakingFlags.unaryPlus() { this@Builder.code = this@Builder.code or code } - operator fun SpeakingFlags.unaryMinus() { + public operator fun SpeakingFlags.unaryMinus() { if (this@Builder.code and code == code) { this@Builder.code = this@Builder.code xor code } } - fun flags() = SpeakingFlags(code) + public fun flags(): SpeakingFlags = SpeakingFlags(code) } } @KordVoice @OptIn(ExperimentalContracts::class) -inline fun SpeakingFlags(builder: SpeakingFlags.Builder.() -> Unit): SpeakingFlags { +public inline fun SpeakingFlags(builder: SpeakingFlags.Builder.() -> Unit): SpeakingFlags { contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } return SpeakingFlags.Builder().apply(builder).flags() } @KordVoice -fun SpeakingFlags(vararg flags: SpeakingFlag) = SpeakingFlags { +public fun SpeakingFlags(vararg flags: SpeakingFlag): SpeakingFlags = SpeakingFlags { flags.forEach { +it } } @KordVoice -fun SpeakingFlags(vararg flags: SpeakingFlags) = SpeakingFlags { +public fun SpeakingFlags(vararg flags: SpeakingFlags): SpeakingFlags = SpeakingFlags { flags.forEach { +it } } @KordVoice -fun SpeakingFlags(flags: Iterable) = SpeakingFlags { +public fun SpeakingFlags(flags: Iterable): SpeakingFlags = SpeakingFlags { flags.forEach { +it } } @KordVoice @JvmName("SpeakingFlagsWithIterable") -fun SpeakingFlags(flags: Iterable) = SpeakingFlags { +public fun SpeakingFlags(flags: Iterable): SpeakingFlags = SpeakingFlags { flags.forEach { +it } } diff --git a/voice/src/main/kotlin/VoiceConnection.kt b/voice/src/main/kotlin/VoiceConnection.kt index 5ccf6bd21138..297be7840e76 100644 --- a/voice/src/main/kotlin/VoiceConnection.kt +++ b/voice/src/main/kotlin/VoiceConnection.kt @@ -17,8 +17,6 @@ import kotlinx.coroutines.* import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext /** * Data that represents a [VoiceConnection], these will never change during the lifetime of a [VoiceConnection]. @@ -27,7 +25,7 @@ import kotlin.coroutines.EmptyCoroutineContext * @param guildId the id of the guild that the bot is connecting to. * @param sessionId the id of the current voice session, given by Discord. */ -data class VoiceConnectionData( +public data class VoiceConnectionData( val selfId: Snowflake, val guildId: Snowflake, val sessionId: String, @@ -46,19 +44,19 @@ data class VoiceConnectionData( * @param frameInterceptorFactory a factory for [FrameInterceptor]s that is used whenever audio is ready to be sent. See [FrameInterceptor] and [DefaultFrameInterceptor]. */ @KordVoice -class VoiceConnection( - val data: VoiceConnectionData, - val gateway: Gateway, - val voiceGateway: VoiceGateway, - val socket: VoiceUdpSocket, - var voiceGatewayConfiguration: VoiceGatewayConfiguration, - val streams: Streams, - val audioProvider: AudioProvider, - val frameSender: AudioFrameSender, - val nonceStrategy: NonceStrategy, - val frameInterceptorFactory: (FrameInterceptorContext) -> FrameInterceptor, +public class VoiceConnection( + public val data: VoiceConnectionData, + public val gateway: Gateway, + public val voiceGateway: VoiceGateway, + public val socket: VoiceUdpSocket, + public var voiceGatewayConfiguration: VoiceGatewayConfiguration, + public val streams: Streams, + public val audioProvider: AudioProvider, + public val frameSender: AudioFrameSender, + public val nonceStrategy: NonceStrategy, + public val frameInterceptorFactory: (FrameInterceptorContext) -> FrameInterceptor, ) { - val scope: CoroutineScope = + public val scope: CoroutineScope = CoroutineScope(SupervisorJob() + CoroutineName("kord-voice-connection[${data.guildId.value}]")) init { @@ -73,7 +71,7 @@ class VoiceConnection( * Starts the [VoiceGateway] for this [VoiceConnection]. * This will begin the process for audio transmission. */ - suspend fun connect(scope: CoroutineScope = this.scope) { + public suspend fun connect(scope: CoroutineScope = this.scope) { scope.launch { voiceGateway.start(voiceGatewayConfiguration) } @@ -82,7 +80,7 @@ class VoiceConnection( /** * Disconnects from the voice servers, does not change the voice state. */ - suspend fun disconnect() { + public suspend fun disconnect() { voiceGateway.stop() socket.stop() } @@ -90,7 +88,7 @@ class VoiceConnection( /** * Disconnects from Discord voice servers, and leaves the voice channel. */ - suspend fun leave() { + public suspend fun leave() { gateway.send( UpdateVoiceStatus( guildId = data.guildId, @@ -106,7 +104,7 @@ class VoiceConnection( /** * Releases all resources related to this VoiceConnection (except [gateway]) and then stops its CoroutineScope. */ - suspend fun shutdown() { + public suspend fun shutdown() { leave() voiceGateway.detach() @@ -129,7 +127,7 @@ class VoiceConnection( */ @KordVoice @OptIn(ExperimentalContracts::class) -suspend inline fun VoiceConnection( +public suspend inline fun VoiceConnection( gateway: Gateway, selfId: Snowflake, channelId: Snowflake, diff --git a/voice/src/main/kotlin/VoiceConnectionBuilder.kt b/voice/src/main/kotlin/VoiceConnectionBuilder.kt index 53eff201a940..0b3d7ab0b76e 100644 --- a/voice/src/main/kotlin/VoiceConnectionBuilder.kt +++ b/voice/src/main/kotlin/VoiceConnectionBuilder.kt @@ -26,35 +26,35 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.withTimeoutOrNull @KordVoice -class VoiceConnectionBuilder( - var gateway: Gateway, - var selfId: Snowflake, - var channelId: Snowflake, - var guildId: Snowflake +public class VoiceConnectionBuilder( + public var gateway: Gateway, + public var selfId: Snowflake, + public var channelId: Snowflake, + public var guildId: Snowflake ) { /** * The amount in milliseconds to wait for the events required to create a [VoiceConnection]. Default is 5000, or 5 seconds. */ - var timeout: Long = 5000 + public var timeout: Long = 5000 /** * The [AudioProvider] for this [VoiceConnection]. No audio will be provided when one is not set. */ - var audioProvider: AudioProvider? = null + public var audioProvider: AudioProvider? = null /** * The [dev.kord.voice.udp.AudioFrameSender] for this [VoiceConnection]. If null, [dev.kord.voice.udp.DefaultAudioFrameSender] * will be used. */ - var audioSender: AudioFrameSender? = null + public var audioSender: AudioFrameSender? = null /** * The nonce strategy to be used for the encryption of audio packets. * If `null`, [dev.kord.voice.encryption.strategies.LiteNonceStrategy] will be used. */ - var nonceStrategy: NonceStrategy? = null + public var nonceStrategy: NonceStrategy? = null - fun audioProvider(provider: AudioProvider) { + public fun audioProvider(provider: AudioProvider) { this.audioProvider = provider } @@ -63,45 +63,45 @@ class VoiceConnectionBuilder( * When one is not set, a factory will be used to create the default interceptor, see [DefaultFrameInterceptor]. * This factory will be used to create a new [FrameInterceptor] whenever audio is ready to be sent. */ - var frameInterceptorFactory: ((FrameInterceptorContext) -> FrameInterceptor)? = null + public var frameInterceptorFactory: ((FrameInterceptorContext) -> FrameInterceptor)? = null - fun frameInterceptor(factory: (FrameInterceptorContext) -> FrameInterceptor) { + public fun frameInterceptor(factory: (FrameInterceptorContext) -> FrameInterceptor) { this.frameInterceptorFactory = factory } /** * A boolean indicating whether your voice state will be muted. */ - var selfMute: Boolean = false + public var selfMute: Boolean = false /** * A boolean indicating whether your voice state will be deafened. */ - var selfDeaf: Boolean = false + public var selfDeaf: Boolean = false private var voiceGatewayBuilder: (DefaultVoiceGatewayBuilder.() -> Unit)? = null /** * A [dev.kord.voice.udp.VoiceUdpSocket] implementation to be used. If null, a default will be used. */ - var udpSocket: VoiceUdpSocket? = null + public var udpSocket: VoiceUdpSocket? = null /** * A flag to control the implementation of [streams]. Set to false by default. * When set to false, a NOP implementation will be used. * When set to true, a proper receiving implementation will be used. */ - var receiveVoice: Boolean = false + public var receiveVoice: Boolean = false /** * A [Streams] implementation to be used. This will override the [receiveVoice] flag. */ - var streams: Streams? = null + public var streams: Streams? = null /** * A builder to customize the voice connection's underlying [VoiceGateway]. */ - fun voiceGateway(builder: DefaultVoiceGatewayBuilder.() -> Unit) { + public fun voiceGateway(builder: DefaultVoiceGatewayBuilder.() -> Unit) { this.voiceGatewayBuilder = builder } @@ -152,7 +152,7 @@ class VoiceConnectionBuilder( /** * @throws dev.kord.voice.exception.VoiceConnectionInitializationException when there was a problem retrieving voice information from Discord. */ - suspend fun build(): VoiceConnection { + public suspend fun build(): VoiceConnection { val (voiceConnectionData, initialGatewayConfiguration) = gateway.updateVoiceState() val voiceGateway = DefaultVoiceGatewayBuilder(selfId, guildId, voiceConnectionData.sessionId) diff --git a/voice/src/main/kotlin/encryption/XSalsa20Poly1305Codec.kt b/voice/src/main/kotlin/encryption/XSalsa20Poly1305Codec.kt index f257c27386e2..5e1083e2c294 100644 --- a/voice/src/main/kotlin/encryption/XSalsa20Poly1305Codec.kt +++ b/voice/src/main/kotlin/encryption/XSalsa20Poly1305Codec.kt @@ -4,23 +4,45 @@ import com.iwebpp.crypto.TweetNaclFast import dev.kord.voice.io.MutableByteArrayCursor import dev.kord.voice.io.mutableCursor -class XSalsa20Poly1305Codec(val key: ByteArray) { +public class XSalsa20Poly1305Codec(public val key: ByteArray) { private val encryption = XSalsa20Poly1305Encryption(key) - fun encrypt(message: ByteArray, mOffset: Int = 0, mLength: Int = message.size, nonce: ByteArray, output: MutableByteArrayCursor): Boolean = + public fun encrypt( + message: ByteArray, + mOffset: Int = 0, + mLength: Int = message.size, + nonce: ByteArray, + output: MutableByteArrayCursor + ): Boolean = encryption.box(message, mOffset, mLength, nonce, output) - fun decrypt(box: ByteArray, boxOffset: Int = 0, boxLength: Int = box.size, nonce: ByteArray, output: MutableByteArrayCursor): Boolean = + public fun decrypt( + box: ByteArray, + boxOffset: Int = 0, + boxLength: Int = box.size, + nonce: ByteArray, + output: MutableByteArrayCursor + ): Boolean = encryption.open(box, boxOffset, boxLength, nonce, output) } -fun XSalsa20Poly1305Codec.encrypt(message: ByteArray, mOffset: Int = 0, mLength: Int = message.size, nonce: ByteArray): ByteArray? { +public fun XSalsa20Poly1305Codec.encrypt( + message: ByteArray, + mOffset: Int = 0, + mLength: Int = message.size, + nonce: ByteArray +): ByteArray? { val buffer = ByteArray(mLength + TweetNaclFast.SecretBox.boxzerobytesLength) if (!encrypt(message, mOffset, mLength, nonce, buffer.mutableCursor())) return null return buffer } -fun XSalsa20Poly1305Codec.decrypt(box: ByteArray, boxOffset: Int = 0, boxLength: Int = box.size, nonce: ByteArray): ByteArray? { +public fun XSalsa20Poly1305Codec.decrypt( + box: ByteArray, + boxOffset: Int = 0, + boxLength: Int = box.size, + nonce: ByteArray +): ByteArray? { val buffer = ByteArray(boxLength - TweetNaclFast.SecretBox.boxzerobytesLength) if (!decrypt(box, boxOffset, boxLength, nonce, buffer.mutableCursor())) return null return buffer diff --git a/voice/src/main/kotlin/encryption/strategies/LiteNonceStrategy.kt b/voice/src/main/kotlin/encryption/strategies/LiteNonceStrategy.kt index b47ad36408a4..1e20812698b0 100644 --- a/voice/src/main/kotlin/encryption/strategies/LiteNonceStrategy.kt +++ b/voice/src/main/kotlin/encryption/strategies/LiteNonceStrategy.kt @@ -7,7 +7,7 @@ import dev.kord.voice.io.view import dev.kord.voice.udp.RTPPacket import kotlinx.atomicfu.atomic -class LiteNonceStrategy : NonceStrategy { +public class LiteNonceStrategy : NonceStrategy { override val nonceLength: Int = 4 private var count: Int by atomic(0) diff --git a/voice/src/main/kotlin/encryption/strategies/NonceStrategy.kt b/voice/src/main/kotlin/encryption/strategies/NonceStrategy.kt index 158352580f4e..97a89e924e20 100644 --- a/voice/src/main/kotlin/encryption/strategies/NonceStrategy.kt +++ b/voice/src/main/kotlin/encryption/strategies/NonceStrategy.kt @@ -7,24 +7,24 @@ import dev.kord.voice.udp.RTPPacket /** * An [encryption mode, regarding the nonce](https://discord.com/developers/docs/topics/voice-connections#establishing-a-voice-udp-connection-encryption-modes), supported by Discord. */ -sealed interface NonceStrategy { +public sealed interface NonceStrategy { /** * The amount of bytes this nonce will take up. */ - val nonceLength: Int + public val nonceLength: Int /** * Reads the nonce from this [packet] (also removes it if it resides in the payload), and returns a [ByteArrayView] of it. */ - fun strip(packet: RTPPacket): ByteArrayView + public fun strip(packet: RTPPacket): ByteArrayView /** * Generates a nonce, may use the provided information. */ - fun generate(header: () -> ByteArrayView): ByteArrayView + public fun generate(header: () -> ByteArrayView): ByteArrayView /** * Writes the [nonce] to [cursor] in the correct relative position. */ - fun append(nonce: ByteArrayView, cursor: MutableByteArrayCursor) + public fun append(nonce: ByteArrayView, cursor: MutableByteArrayCursor) } \ No newline at end of file diff --git a/voice/src/main/kotlin/encryption/strategies/NormalNonceStrategy.kt b/voice/src/main/kotlin/encryption/strategies/NormalNonceStrategy.kt index 5d29e6bdc372..a01940ffd9b4 100644 --- a/voice/src/main/kotlin/encryption/strategies/NormalNonceStrategy.kt +++ b/voice/src/main/kotlin/encryption/strategies/NormalNonceStrategy.kt @@ -7,7 +7,7 @@ import dev.kord.voice.io.view import dev.kord.voice.udp.RTPPacket import dev.kord.voice.udp.RTP_HEADER_LENGTH -class NormalNonceStrategy : NonceStrategy { +public class NormalNonceStrategy : NonceStrategy { // the nonce is already a part of the rtp header, which means this will take up no extra space. override val nonceLength: Int = 0 diff --git a/voice/src/main/kotlin/encryption/strategies/SuffixNonceStrategy.kt b/voice/src/main/kotlin/encryption/strategies/SuffixNonceStrategy.kt index 1514a3641e5a..a43a73c315d6 100644 --- a/voice/src/main/kotlin/encryption/strategies/SuffixNonceStrategy.kt +++ b/voice/src/main/kotlin/encryption/strategies/SuffixNonceStrategy.kt @@ -8,7 +8,7 @@ import kotlin.random.Random private const val SUFFIX_NONCE_LENGTH = 24 -class SuffixNonceStrategy : NonceStrategy { +public class SuffixNonceStrategy : NonceStrategy { override val nonceLength: Int = SUFFIX_NONCE_LENGTH private val nonceBuffer: ByteArray = ByteArray(SUFFIX_NONCE_LENGTH) diff --git a/voice/src/main/kotlin/exception/VoiceConnectionInitializationException.kt b/voice/src/main/kotlin/exception/VoiceConnectionInitializationException.kt index 8e19e531a175..d5a16cde4db1 100644 --- a/voice/src/main/kotlin/exception/VoiceConnectionInitializationException.kt +++ b/voice/src/main/kotlin/exception/VoiceConnectionInitializationException.kt @@ -1,7 +1,7 @@ package dev.kord.voice.exception -class VoiceConnectionInitializationException : Exception { - constructor(message: String) : super(message) - constructor(cause: Throwable) : super(cause) - constructor(message: String, cause: Throwable) : super(message, cause) +public class VoiceConnectionInitializationException : Exception { + public constructor(message: String) : super(message) + public constructor(cause: Throwable) : super(cause) + public constructor(message: String, cause: Throwable) : super(message, cause) } diff --git a/voice/src/main/kotlin/gateway/Command.kt b/voice/src/main/kotlin/gateway/Command.kt index 2e12f8d44bb6..d1a1fb571b54 100644 --- a/voice/src/main/kotlin/gateway/Command.kt +++ b/voice/src/main/kotlin/gateway/Command.kt @@ -11,8 +11,8 @@ import kotlinx.serialization.descriptors.buildClassSerialDescriptor import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.JsonObject -sealed class Command { - companion object : SerializationStrategy { +public sealed class Command { + internal companion object : SerializationStrategy { override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Command") { element("op", OpCode.descriptor) element("d", JsonObject.serializer().descriptor) @@ -50,7 +50,7 @@ sealed class Command { } @Serializable -data class Identify( +public data class Identify( @SerialName("server_id") val serverId: Snowflake, @SerialName("user_id") @@ -61,22 +61,22 @@ data class Identify( ) : Command() @Serializable -data class Heartbeat(val nonce: Long) : Command() +public data class Heartbeat(val nonce: Long) : Command() @Serializable -data class SendSpeaking( +public data class SendSpeaking( val speaking: SpeakingFlags, val delay: Int, val ssrc: UInt ) : Command() @Serializable -data class SelectProtocol( +public data class SelectProtocol( val protocol: String, val data: Data ) : Command() { @Serializable - data class Data( + public data class Data( val address: String, val port: Int, val mode: EncryptionMode @@ -84,7 +84,7 @@ data class SelectProtocol( } @Serializable -data class Resume( +public data class Resume( val serverId: Snowflake, val sessionId: String, val token: String diff --git a/voice/src/main/kotlin/gateway/DefaultVoiceGateway.kt b/voice/src/main/kotlin/gateway/DefaultVoiceGateway.kt index 8adb17505e3d..3c7ccdf1cba8 100644 --- a/voice/src/main/kotlin/gateway/DefaultVoiceGateway.kt +++ b/voice/src/main/kotlin/gateway/DefaultVoiceGateway.kt @@ -31,7 +31,7 @@ private sealed class State(val retry: Boolean) { } @KordVoice -data class DefaultVoiceGatewayData( +public data class DefaultVoiceGatewayData( val selfId: Snowflake, val guildId: Snowflake, val sessionId: String, @@ -44,7 +44,7 @@ data class DefaultVoiceGatewayData( * The default Voice Gateway implementation of Kord, using an [HttpClient] for the underlying websocket. */ @KordVoice -class DefaultVoiceGateway( +public class DefaultVoiceGateway( private val data: DefaultVoiceGatewayData ) : VoiceGateway { override val scope: CoroutineScope = @@ -79,7 +79,7 @@ class DefaultVoiceGateway( // prevent race conditions caused by suspending due to the reconnectRetry private val connectMutex = Mutex(locked = false) - override suspend fun start(configuration: VoiceGatewayConfiguration) = connectMutex.withLock { + override suspend fun start(configuration: VoiceGatewayConfiguration): Unit = connectMutex.withLock { resetState(configuration) while (data.reconnectRetry.hasNext && state.value is State.Running) { @@ -173,7 +173,7 @@ class DefaultVoiceGateway( } - override suspend fun send(command: Command) = stateMutex.withLock { + override suspend fun send(command: Command): Unit = stateMutex.withLock { sendUnsafe(command) } diff --git a/voice/src/main/kotlin/gateway/DefaultVoiceGatewayBuilder.kt b/voice/src/main/kotlin/gateway/DefaultVoiceGatewayBuilder.kt index fbdff363f81f..85d55dac19cf 100644 --- a/voice/src/main/kotlin/gateway/DefaultVoiceGatewayBuilder.kt +++ b/voice/src/main/kotlin/gateway/DefaultVoiceGatewayBuilder.kt @@ -15,17 +15,17 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlin.time.Duration @KordVoice -class DefaultVoiceGatewayBuilder( - val selfId: Snowflake, - val guildId: Snowflake, - val sessionId: String, +public class DefaultVoiceGatewayBuilder( + public val selfId: Snowflake, + public val guildId: Snowflake, + public val sessionId: String, ) { - var client: HttpClient? = null - var reconnectRetry: Retry? = null - var eventFlow: MutableSharedFlow = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE) + public var client: HttpClient? = null + public var reconnectRetry: Retry? = null + public var eventFlow: MutableSharedFlow = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE) @OptIn(InternalAPI::class) - fun build(): DefaultVoiceGateway { + public fun build(): DefaultVoiceGateway { val client = client ?: HttpClient(CIO) { install(WebSockets) install(JsonFeature) diff --git a/voice/src/main/kotlin/gateway/OpCode.kt b/voice/src/main/kotlin/gateway/OpCode.kt index 743bfe6d8646..771788dc6143 100644 --- a/voice/src/main/kotlin/gateway/OpCode.kt +++ b/voice/src/main/kotlin/gateway/OpCode.kt @@ -7,7 +7,7 @@ import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder -enum class OpCode(val code: Int) { +public enum class OpCode(public val code: Int) { Unknown(Int.MIN_VALUE), Identify(0), SelectProtocol(1), @@ -21,7 +21,7 @@ enum class OpCode(val code: Int) { Resumed(9), ClientDisconnect(13); - companion object OpCodeSerializer : KSerializer { + internal companion object OpCodeSerializer : KSerializer { override val descriptor: SerialDescriptor get() = PrimitiveSerialDescriptor("op", PrimitiveKind.INT) diff --git a/voice/src/main/kotlin/gateway/Ticker.kt b/voice/src/main/kotlin/gateway/Ticker.kt index 52334959bfeb..59d6e292f67a 100644 --- a/voice/src/main/kotlin/gateway/Ticker.kt +++ b/voice/src/main/kotlin/gateway/Ticker.kt @@ -1,13 +1,12 @@ package dev.kord.voice.gateway import kotlinx.coroutines.* -import mu.KotlinLogging /** * A reusable fixed rate ticker. */ @ObsoleteCoroutinesApi -class Ticker { +internal class Ticker { // we only want one of these private var tickerJob: Job? = null diff --git a/voice/src/main/kotlin/gateway/VoiceEvent.kt b/voice/src/main/kotlin/gateway/VoiceEvent.kt index 03341cc09dd7..8ee64774711c 100644 --- a/voice/src/main/kotlin/gateway/VoiceEvent.kt +++ b/voice/src/main/kotlin/gateway/VoiceEvent.kt @@ -20,8 +20,8 @@ import mu.KotlinLogging private val jsonLogger = KotlinLogging.logger { } -sealed class VoiceEvent { - companion object : DeserializationStrategy { +public sealed class VoiceEvent { + internal companion object : DeserializationStrategy { override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Event") { element("op", OpCode.descriptor) element("d", JsonElement.serializer().descriptor) @@ -75,7 +75,7 @@ sealed class VoiceEvent { } @Serializable -data class Ready( +public data class Ready( val ssrc: UInt, val ip: String, val port: Int, @@ -83,7 +83,7 @@ data class Ready( ) : VoiceEvent() @Serializable -data class Hello( +public data class Hello( @SerialName("v") val version: Short, @SerialName("heartbeat_interval") @@ -91,52 +91,52 @@ data class Hello( ) : VoiceEvent() @Serializable -data class HeartbeatAck(val nonce: Long) : VoiceEvent() +public data class HeartbeatAck(val nonce: Long) : VoiceEvent() @Serializable -data class SessionDescription( +public data class SessionDescription( val mode: EncryptionMode, @SerialName("secret_key") val secretKey: List ) : VoiceEvent() @Serializable -data class Speaking( +public data class Speaking( @SerialName("user_id") val userId: Snowflake, val ssrc: UInt, val speaking: SpeakingFlags -): VoiceEvent() +) : VoiceEvent() @Serializable -object Resumed : VoiceEvent() +public object Resumed : VoiceEvent() -sealed class Close : VoiceEvent() { +public sealed class Close : VoiceEvent() { /** * The user closed the Gateway connection. */ - object UserClose : Close() + public object UserClose : Close() /** * The connection was closed because of a timeout, probably due to a loss of internet connection. */ - object Timeout : Close() + public object Timeout : Close() /** * Discord closed the connection with a [closeCode]. * * @param recoverable true if the gateway will automatically try to reconnect. */ - class DiscordClose(val closeCode: VoiceGatewayCloseCode, val recoverable: Boolean) : Close() + public class DiscordClose(public val closeCode: VoiceGatewayCloseCode, public val recoverable: Boolean) : Close() /** * The gateway closed and will attempt to resume the session. */ - object Reconnecting : Close() + public object Reconnecting : Close() /** * The Gateway has failed to establish a connection too many times and will not try to reconnect anymore. * The user is free to manually connect again using [VoiceGateway.start], otherwise all resources linked to the Gateway should free. */ - object RetryLimitReached : Close() + public object RetryLimitReached : Close() } \ No newline at end of file diff --git a/voice/src/main/kotlin/gateway/VoiceGateway.kt b/voice/src/main/kotlin/gateway/VoiceGateway.kt index 6a47eafb7d64..34341f452479 100644 --- a/voice/src/main/kotlin/gateway/VoiceGateway.kt +++ b/voice/src/main/kotlin/gateway/VoiceGateway.kt @@ -5,6 +5,7 @@ import io.ktor.util.* import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* +import mu.KLogger import mu.KotlinLogging import kotlin.coroutines.EmptyCoroutineContext import kotlin.time.Duration @@ -15,14 +16,14 @@ import kotlin.time.Duration * Allows consumers to receive [VoiceEvent]s through [events] and send [Command]s through [send]. */ @KordVoice -interface VoiceGateway { - val scope: CoroutineScope +public interface VoiceGateway { + public val scope: CoroutineScope /** * The incoming [VoiceEvent]s of the Gateway. Users should expect [kotlinx.coroutines.flow.Flow]s to be hot and remain * open for the entire lifecycle of the Gateway. */ - val events: SharedFlow + public val events: SharedFlow /** * The [Duration] between the last [Heartbeat] and [HeartbeatAck]. @@ -30,7 +31,7 @@ interface VoiceGateway { * This flow will have a [value][StateFlow.value] off `null` if the gateway is not [active][VoiceGateway.start], * or no [HeartbeatAck] has been received yet. */ - val ping: StateFlow + public val ping: StateFlow /** * Starts a reconnection voice gateway connection with the given [configuration]. This function will suspend @@ -38,7 +39,7 @@ interface VoiceGateway { * * @param configuration - the configuration for this gateway session. */ - suspend fun start(configuration: VoiceGatewayConfiguration) + public suspend fun start(configuration: VoiceGatewayConfiguration) /** * Sends a [Command] to the gateway, suspending until the message has been sent. @@ -46,14 +47,14 @@ interface VoiceGateway { * @param command the [Command] to send to the gateway. * @throws Exception when the gateway isn't open/ */ - suspend fun send(command: Command) + public suspend fun send(command: Command) /** * Close the Gateway and ends the current session, suspending until the underlying websocket is closed. */ - suspend fun stop() + public suspend fun stop() - companion object { + public companion object { private object None : VoiceGateway { override val scope: CoroutineScope = CoroutineScope(EmptyCoroutineContext + CoroutineName("None VoiceGateway")) @@ -80,10 +81,10 @@ interface VoiceGateway { /** * Returns a [VoiceGateway] with no-op behavior, an empty [VoiceGateway.events] flow and a ping of [Duration.ZERO]. */ - fun none(): VoiceGateway = None + public fun none(): VoiceGateway = None } - suspend fun detach() + public suspend fun detach() } @@ -91,7 +92,7 @@ interface VoiceGateway { * Logger used to report throwables caught in [VoiceGateway.on]. */ @PublishedApi -internal val voiceGatewayOnLogger = KotlinLogging.logger("Gateway.on") +internal val voiceGatewayOnLogger: KLogger = KotlinLogging.logger("Gateway.on") /** * Convenience method that will invoke the [consumer] on every event [T] created by [VoiceGateway.events]. @@ -105,7 +106,7 @@ internal val voiceGatewayOnLogger = KotlinLogging.logger("Gateway.on") * events for this [consumer]. */ @KordVoice -inline fun VoiceGateway.on( +public inline fun VoiceGateway.on( scope: CoroutineScope = this.scope, crossinline consumer: suspend T.() -> Unit ): Job { @@ -117,23 +118,23 @@ inline fun VoiceGateway.on( /** * Representation of Discord's [Voice Gateway close codes](https://discord.com/developers/docs/topics/opcodes-and-status-codes#voice-voice-close-event-codes). */ -sealed class VoiceGatewayCloseCode(val code: Int) { - class Unknown(code: Int) : VoiceGatewayCloseCode(code) - object UnknownOpcode : VoiceGatewayCloseCode(4001) - object FailedToDecodePayload : VoiceGatewayCloseCode(4002) - object NotAuthenticated : VoiceGatewayCloseCode(4003) - object AuthenticationFailed : VoiceGatewayCloseCode(4004) - object AlreadyAuthenticated : VoiceGatewayCloseCode(4005) - object SessionNoLongerValid : VoiceGatewayCloseCode(4006) - object SessionTimeout : VoiceGatewayCloseCode(4009) - object ServerNotFound : VoiceGatewayCloseCode(4011) - object UnknownProtocol : VoiceGatewayCloseCode(4012) - object Disconnect : VoiceGatewayCloseCode(4014) - object VoiceServerCrashed : VoiceGatewayCloseCode(4015) - object UnknownEncryptionMode : VoiceGatewayCloseCode(4016) - - companion object { - fun of(code: Int) = +public sealed class VoiceGatewayCloseCode(public val code: Int) { + public class Unknown(code: Int) : VoiceGatewayCloseCode(code) + public object UnknownOpcode : VoiceGatewayCloseCode(4001) + public object FailedToDecodePayload : VoiceGatewayCloseCode(4002) + public object NotAuthenticated : VoiceGatewayCloseCode(4003) + public object AuthenticationFailed : VoiceGatewayCloseCode(4004) + public object AlreadyAuthenticated : VoiceGatewayCloseCode(4005) + public object SessionNoLongerValid : VoiceGatewayCloseCode(4006) + public object SessionTimeout : VoiceGatewayCloseCode(4009) + public object ServerNotFound : VoiceGatewayCloseCode(4011) + public object UnknownProtocol : VoiceGatewayCloseCode(4012) + public object Disconnect : VoiceGatewayCloseCode(4014) + public object VoiceServerCrashed : VoiceGatewayCloseCode(4015) + public object UnknownEncryptionMode : VoiceGatewayCloseCode(4016) + + public companion object { + public fun of(code: Int): VoiceGatewayCloseCode = when (code) { 4001 -> UnknownOpcode 4002 -> FailedToDecodePayload diff --git a/voice/src/main/kotlin/gateway/VoiceGatewayConfiguration.kt b/voice/src/main/kotlin/gateway/VoiceGatewayConfiguration.kt index 7925b2c296a3..9633dbd3287f 100644 --- a/voice/src/main/kotlin/gateway/VoiceGatewayConfiguration.kt +++ b/voice/src/main/kotlin/gateway/VoiceGatewayConfiguration.kt @@ -3,7 +3,7 @@ package dev.kord.voice.gateway import dev.kord.common.annotation.KordVoice @KordVoice -data class VoiceGatewayConfiguration( +public data class VoiceGatewayConfiguration( val token: String, val endpoint: String ) \ No newline at end of file diff --git a/voice/src/main/kotlin/io/ByteArrayCursors.kt b/voice/src/main/kotlin/io/ByteArrayCursors.kt index 2acf7f8c1c67..4e8582163d5d 100644 --- a/voice/src/main/kotlin/io/ByteArrayCursors.kt +++ b/voice/src/main/kotlin/io/ByteArrayCursors.kt @@ -6,21 +6,22 @@ import kotlinx.atomicfu.atomic /** * A light-weight mutable cursor for a ByteArray. */ -class MutableByteArrayCursor(data: ByteArray) { - var data: ByteArray = data +public class MutableByteArrayCursor(data: ByteArray) { + public var data: ByteArray = data private set - var cursor by atomic(0) + public var cursor: Int by atomic(0) - val remaining get() = data.size - cursor + public val remaining: Int + get() = data.size - cursor - val isExhausted: Boolean = cursor == data.size + 1 + public val isExhausted: Boolean = cursor == data.size + 1 - fun reset() { + public fun reset() { cursor = 0 } - fun resize(newSize: Int, ifSmaller: Boolean = false): Boolean { + public fun resize(newSize: Int, ifSmaller: Boolean = false): Boolean { return if (data.size < newSize || ifSmaller) { val newData = ByteArray(newSize) @@ -43,14 +44,14 @@ class MutableByteArrayCursor(data: ByteArray) { return } - fun writeByte(b: Byte) { + public fun writeByte(b: Byte) { isNotExhaustedOrThrow() data[cursor] = b cursor++ } - fun writeByteArray(array: ByteArray, offset: Int = 0, length: Int = array.size) { + public fun writeByteArray(array: ByteArray, offset: Int = 0, length: Int = array.size) { if (length > remaining) error("$remaining bytes remaining. tried to write $length bytes") array.copyInto(data, cursor, offset, offset + length) @@ -58,9 +59,9 @@ class MutableByteArrayCursor(data: ByteArray) { cursor += length } - fun writeByteView(view: ByteArrayView) = writeByteArray(view.data, view.dataStart, view.viewSize) + public fun writeByteView(view: ByteArrayView): Unit = writeByteArray(view.data, view.dataStart, view.viewSize) - fun writeShort(s: Short) { + public fun writeShort(s: Short) { var value = s.reverseByteOrder().toInt() repeat(2) { @@ -69,7 +70,7 @@ class MutableByteArrayCursor(data: ByteArray) { } } - fun writeInt(i: Int) { + public fun writeInt(i: Int) { var value = i.reverseByteOrder() repeat(4) { @@ -79,17 +80,18 @@ class MutableByteArrayCursor(data: ByteArray) { } } -fun ByteArray.mutableCursor() = MutableByteArrayCursor(this) -fun ByteArrayView.mutableCursor() = MutableByteArrayCursor(data).also { it.cursor = dataStart } +public fun ByteArray.mutableCursor(): MutableByteArrayCursor = MutableByteArrayCursor(this) +public fun ByteArrayView.mutableCursor(): MutableByteArrayCursor = + MutableByteArrayCursor(data).also { it.cursor = dataStart } -fun MutableByteArrayCursor.writeByteArrayOrResize(data: ByteArray) { +public fun MutableByteArrayCursor.writeByteArrayOrResize(data: ByteArray) { if (remaining < data.size) resize(data.size + this.data.size) writeByteArray(data) } -fun MutableByteArrayCursor.writeByteViewOrResize(view: ByteArrayView) { +public fun MutableByteArrayCursor.writeByteViewOrResize(view: ByteArrayView) { if (remaining < view.viewSize) resize(view.viewSize + data.size) @@ -99,27 +101,27 @@ fun MutableByteArrayCursor.writeByteViewOrResize(view: ByteArrayView) { /** * A lightweight read-only cursor for a ByteArrayView. */ -class ReadableByteArrayCursor(val view: ByteArrayView) { - var cursor: Int by atomic(0) +public class ReadableByteArrayCursor(public val view: ByteArrayView) { + public var cursor: Int by atomic(0) - val remaining: Int get() = view.data.size - cursor + public val remaining: Int get() = view.data.size - cursor private fun hasEnoughOrThrow(n: Int) { if (view.viewSize >= cursor + n) return else error("not enough bytes") } - fun readByte(): Byte { + public fun readByte(): Byte { hasEnoughOrThrow(1) return view[cursor++] } - fun consume(n: Int) { + public fun consume(n: Int) { cursor += n } - fun readBytes(n: Int): ByteArrayView { + public fun readBytes(n: Int): ByteArrayView { hasEnoughOrThrow(n) val view = view.view(view.dataStart + cursor, view.dataStart + cursor + n)!! @@ -128,22 +130,22 @@ class ReadableByteArrayCursor(val view: ByteArrayView) { return view } - fun readShort(): Short { + public fun readShort(): Short { hasEnoughOrThrow(2) return readBytes(2).asShort() } - fun readInt(): Int { + public fun readInt(): Int { hasEnoughOrThrow(4) return readBytes(4).asInt() } - fun readRemaining(): ByteArrayView { + public fun readRemaining(): ByteArrayView { return readBytes(remaining) } } -fun ByteArray.readableCursor() = view().readableCursor() -fun ByteArrayView.readableCursor() = ReadableByteArrayCursor(this) \ No newline at end of file +public fun ByteArray.readableCursor(): ReadableByteArrayCursor = view().readableCursor() +public fun ByteArrayView.readableCursor(): ReadableByteArrayCursor = ReadableByteArrayCursor(this) \ No newline at end of file diff --git a/voice/src/main/kotlin/io/ByteArrayView.kt b/voice/src/main/kotlin/io/ByteArrayView.kt index 252de65dca50..fdc44913b850 100644 --- a/voice/src/main/kotlin/io/ByteArrayView.kt +++ b/voice/src/main/kotlin/io/ByteArrayView.kt @@ -3,9 +3,9 @@ package dev.kord.voice.io /** * A lightweight read-only view of a ByteArray. */ -class ByteArrayView private constructor(val data: ByteArray, start: Int, end: Int) : Iterable { - companion object { - fun from(data: ByteArray, start: Int, end: Int) = +public class ByteArrayView private constructor(public val data: ByteArray, start: Int, end: Int) : Iterable { + public companion object { + public fun from(data: ByteArray, start: Int, end: Int): ByteArrayView? = if ((0 <= start && start <= data.size) && (start <= end && end <= data.size)) ByteArrayView( data, start, @@ -14,15 +14,15 @@ class ByteArrayView private constructor(val data: ByteArray, start: Int, end: In else null } - var dataStart: Int = start + public var dataStart: Int = start private set - var dataEnd: Int = end + public var dataEnd: Int = end private set - val viewSize: Int get() = dataEnd - dataStart + public val viewSize: Int get() = dataEnd - dataStart - operator fun get(index: Int): Byte { + public operator fun get(index: Int): Byte { if (dataStart + index > dataEnd) { throw ArrayIndexOutOfBoundsException(index) } @@ -47,7 +47,7 @@ class ByteArrayView private constructor(val data: ByteArray, start: Int, end: In override operator fun iterator(): Iterator = ByteArrayViewIterator(this) - fun asShort(): Short { + public fun asShort(): Short { require(viewSize == 2) { "this view must be equal to 2 bytes to read as short. instead the size is $viewSize" } var value = 0 @@ -58,7 +58,7 @@ class ByteArrayView private constructor(val data: ByteArray, start: Int, end: In return value.toShort() } - fun asInt(): Int { + public fun asInt(): Int { require(viewSize == 4) { "this view must be equal to 4 bytes to read as int. instead the size is $viewSize" } var value = 0 @@ -69,7 +69,7 @@ class ByteArrayView private constructor(val data: ByteArray, start: Int, end: In return value } - fun resize(start: Int = this.dataStart, end: Int = this.dataEnd): Boolean { + public fun resize(start: Int = this.dataStart, end: Int = this.dataEnd): Boolean { // check if the start and end is within bounds and are in the correct order return if (start >= 0 && end <= data.size) { this.dataStart = start @@ -81,21 +81,21 @@ class ByteArrayView private constructor(val data: ByteArray, start: Int, end: In } } - fun view(start: Int = dataStart, end: Int = dataEnd): ByteArrayView? { + public fun view(start: Int = dataStart, end: Int = dataEnd): ByteArrayView? { return from(data, start, end) } /** * Create a new [ByteArray] that's data contains only this view. */ - fun toByteArray(): ByteArray { + public fun toByteArray(): ByteArray { return data.copyOfRange(dataStart, dataEnd) } /** * Creates a new [ByteArrayView] that's data contains only this view. */ - fun clone(): ByteArrayView { + public fun clone(): ByteArrayView { return toByteArray().view() } @@ -104,5 +104,5 @@ class ByteArrayView private constructor(val data: ByteArray, start: Int, end: In } } -fun ByteArray.view(start: Int, end: Int) = ByteArrayView.from(this, start, end) -fun ByteArray.view() = view(0, size)!! \ No newline at end of file +public fun ByteArray.view(start: Int, end: Int): ByteArrayView? = ByteArrayView.from(this, start, end) +public fun ByteArray.view(): ByteArrayView = view(0, size)!! \ No newline at end of file diff --git a/voice/src/main/kotlin/streams/DefaultStreams.kt b/voice/src/main/kotlin/streams/DefaultStreams.kt index f30c91c12b91..b7860a50209c 100644 --- a/voice/src/main/kotlin/streams/DefaultStreams.kt +++ b/voice/src/main/kotlin/streams/DefaultStreams.kt @@ -25,7 +25,7 @@ import mu.KotlinLogging private val defaultStreamsLogger = KotlinLogging.logger { } @KordVoice -class DefaultStreams( +public class DefaultStreams( private val voiceGateway: VoiceGateway, private val udp: VoiceUdpSocket, private val nonceStrategy: NonceStrategy diff --git a/voice/src/main/kotlin/streams/NOPStreams.kt b/voice/src/main/kotlin/streams/NOPStreams.kt index d37604d363f7..2428d45d31ef 100644 --- a/voice/src/main/kotlin/streams/NOPStreams.kt +++ b/voice/src/main/kotlin/streams/NOPStreams.kt @@ -9,11 +9,11 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow @KordVoice -object NOPStreams : Streams { - override suspend fun listen(key: ByteArray, server: NetworkAddress) { } +public object NOPStreams : Streams { + override suspend fun listen(key: ByteArray, server: NetworkAddress) {} - override val incomingAudioPackets: Flow = flow { } - override val incomingAudioFrames: Flow> = flow { } - override val incomingUserStreams: Flow> = flow { } + override val incomingAudioPackets: Flow = flow { } + override val incomingAudioFrames: Flow> = flow { } + override val incomingUserStreams: Flow> = flow { } override val ssrcToUser: Map = emptyMap() } \ No newline at end of file diff --git a/voice/src/main/kotlin/streams/Streams.kt b/voice/src/main/kotlin/streams/Streams.kt index da322e5d3a37..b9a67e36dd05 100644 --- a/voice/src/main/kotlin/streams/Streams.kt +++ b/voice/src/main/kotlin/streams/Streams.kt @@ -11,30 +11,30 @@ import kotlinx.coroutines.flow.Flow * A representation of receiving voice through Discord and different stages of processing. */ @KordVoice -interface Streams { +public interface Streams { /** * Starts propagating packets from [server] with the following [key] to decrypt the incoming frames. */ - suspend fun listen(key: ByteArray, server: NetworkAddress) + public suspend fun listen(key: ByteArray, server: NetworkAddress) /** * A flow of all incoming [dev.kord.voice.udp.RTPPacket]s through the UDP connection. */ - val incomingAudioPackets: Flow + public val incomingAudioPackets: Flow /** * A flow of all incoming [AudioFrame]s mapped to their [ssrc][UInt]. */ - val incomingAudioFrames: Flow> + public val incomingAudioFrames: Flow> /** * A flow of all incoming [AudioFrame]s mapped to their [userId][Snowflake]. * Streams for every user should be built over time and will not be immediately available. */ - val incomingUserStreams: Flow> + public val incomingUserStreams: Flow> /** * A map of [ssrc][UInt]s to their corresponding [userId][Snowflake]. */ - val ssrcToUser: Map + public val ssrcToUser: Map } \ No newline at end of file diff --git a/voice/src/main/kotlin/udp/AudioFrameSender.kt b/voice/src/main/kotlin/udp/AudioFrameSender.kt index 7ca290593f68..0b0ad7f00176 100644 --- a/voice/src/main/kotlin/udp/AudioFrameSender.kt +++ b/voice/src/main/kotlin/udp/AudioFrameSender.kt @@ -11,7 +11,7 @@ import dev.kord.voice.encryption.strategies.NonceStrategy import io.ktor.util.network.* @KordVoice -data class AudioFrameSenderConfiguration( +public data class AudioFrameSenderConfiguration( val server: NetworkAddress, val ssrc: UInt, val key: ByteArray, @@ -22,10 +22,10 @@ data class AudioFrameSenderConfiguration( ) @KordVoice -interface AudioFrameSender { +public interface AudioFrameSender { /** * This should start polling frames from [the audio provider][AudioFrameSenderConfiguration.provider] and * send them to Discord. */ - suspend fun start(configuration: AudioFrameSenderConfiguration) + public suspend fun start(configuration: AudioFrameSenderConfiguration) } \ No newline at end of file diff --git a/voice/src/main/kotlin/udp/AudioPacketProvider.kt b/voice/src/main/kotlin/udp/AudioPacketProvider.kt index 209d30f52d59..b58bb0514f70 100644 --- a/voice/src/main/kotlin/udp/AudioPacketProvider.kt +++ b/voice/src/main/kotlin/udp/AudioPacketProvider.kt @@ -8,14 +8,14 @@ import dev.kord.voice.io.MutableByteArrayCursor import dev.kord.voice.io.mutableCursor import dev.kord.voice.io.view -abstract class AudioPacketProvider(val key: ByteArray, val nonceStrategy: NonceStrategy) { - abstract fun provide(sequence: UShort, timestamp: UInt, ssrc: UInt, data: ByteArray): ByteArrayView +public abstract class AudioPacketProvider(public val key: ByteArray, public val nonceStrategy: NonceStrategy) { + public abstract fun provide(sequence: UShort, timestamp: UInt, ssrc: UInt, data: ByteArray): ByteArrayView } private class CouldNotEncryptDataException(val data: ByteArray) : RuntimeException("Couldn't encrypt the following data: [${data.joinToString(", ")}]") -class DefaultAudioPackerProvider(key: ByteArray, nonceStrategy: NonceStrategy) : +public class DefaultAudioPackerProvider(key: ByteArray, nonceStrategy: NonceStrategy) : AudioPacketProvider(key, nonceStrategy) { private val codec = XSalsa20Poly1305Codec(key) diff --git a/voice/src/main/kotlin/udp/DefaultAudioFrameSender.kt b/voice/src/main/kotlin/udp/DefaultAudioFrameSender.kt index f4a53cc7caa6..1676adac11d4 100644 --- a/voice/src/main/kotlin/udp/DefaultAudioFrameSender.kt +++ b/voice/src/main/kotlin/udp/DefaultAudioFrameSender.kt @@ -14,13 +14,13 @@ import kotlin.random.Random private val audioFrameSenderLogger = KotlinLogging.logger { } @KordVoice -data class DefaultAudioFrameSenderData( +public data class DefaultAudioFrameSenderData( val udp: VoiceUdpSocket ) @KordVoice -class DefaultAudioFrameSender( - val data: DefaultAudioFrameSenderData +public class DefaultAudioFrameSender( + public val data: DefaultAudioFrameSenderData ) : AudioFrameSender { private fun createFrameInterceptor(configuration: AudioFrameSenderConfiguration): FrameInterceptor = with(configuration) { diff --git a/voice/src/main/kotlin/udp/GlobalVoiceUdpSocket.kt b/voice/src/main/kotlin/udp/GlobalVoiceUdpSocket.kt index fb3429950e4c..5a896b8fc92c 100644 --- a/voice/src/main/kotlin/udp/GlobalVoiceUdpSocket.kt +++ b/voice/src/main/kotlin/udp/GlobalVoiceUdpSocket.kt @@ -20,8 +20,9 @@ private val globalVoiceSocketLogger = KotlinLogging.logger { } * Initiated once and kept open for the lifetime of this process. */ @KordVoice -object GlobalVoiceUdpSocket : VoiceUdpSocket { - private val socketScope = CoroutineScope(Dispatchers.Default + SupervisorJob() + CoroutineName("kord-voice-global-socket")) +public object GlobalVoiceUdpSocket : VoiceUdpSocket { + private val socketScope = + CoroutineScope(Dispatchers.Default + SupervisorJob() + CoroutineName("kord-voice-global-socket")) private val _incoming: MutableSharedFlow = MutableSharedFlow() override val incoming: SharedFlow = _incoming diff --git a/voice/src/main/kotlin/udp/PayloadType.kt b/voice/src/main/kotlin/udp/PayloadType.kt index 7bd5098f806f..45fa0fb85644 100644 --- a/voice/src/main/kotlin/udp/PayloadType.kt +++ b/voice/src/main/kotlin/udp/PayloadType.kt @@ -3,13 +3,13 @@ package dev.kord.voice.udp /** * A guesstimated list of known Discord RTP payloads. */ -sealed class PayloadType(val raw: Byte) { - object Alive : PayloadType(0x37.toByte()) - object Audio : PayloadType(0x78.toByte()) - class Unknown(value: Byte) : PayloadType(value) +public sealed class PayloadType(public val raw: Byte) { + public object Alive : PayloadType(0x37.toByte()) + public object Audio : PayloadType(0x78.toByte()) + public class Unknown(value: Byte) : PayloadType(value) - companion object { - fun from(value: Byte) = when (value) { + public companion object { + public fun from(value: Byte): PayloadType = when (value) { 0x37.toByte() -> Alive 0x78.toByte() -> Audio else -> Unknown(value) diff --git a/voice/src/main/kotlin/udp/RTPPacket.kt b/voice/src/main/kotlin/udp/RTPPacket.kt index 45919f271e68..60a6b4330a6d 100644 --- a/voice/src/main/kotlin/udp/RTPPacket.kt +++ b/voice/src/main/kotlin/udp/RTPPacket.kt @@ -7,7 +7,7 @@ import dev.kord.voice.io.view import io.ktor.utils.io.core.* import kotlin.experimental.and -const val RTP_HEADER_LENGTH = 12 +internal const val RTP_HEADER_LENGTH = 12 /** * Originally from [this GitHub library](https://github.com/vidtec/rtp-packet/blob/0b54fdeab5666089215b0074c64a6735b8937f8d/src/main/java/org/vidtec/rfc3550/rtp/RTPPacket.java). @@ -25,7 +25,7 @@ const val RTP_HEADER_LENGTH = 12 */ @Suppress("ArrayInDataClass") @OptIn(ExperimentalUnsignedTypes::class) -data class RTPPacket( +public data class RTPPacket( val paddingBytes: UByte, val payloadType: Byte, val sequence: UShort, @@ -36,10 +36,10 @@ data class RTPPacket( val hasExtension: Boolean, val payload: ByteArrayView, ) { - companion object { - const val VERSION = 2 + public companion object { + internal const val VERSION = 2 - fun fromPacket(packet: ByteReadPacket): RTPPacket? = with(packet) base@{ + public fun fromPacket(packet: ByteReadPacket): RTPPacket? = with(packet) base@{ if (remaining <= 13) return@base null /* @@ -101,7 +101,7 @@ data class RTPPacket( } } - fun clone(): RTPPacket { + public fun clone(): RTPPacket { return RTPPacket( paddingBytes, payloadType, @@ -115,13 +115,13 @@ data class RTPPacket( ) } - fun writeHeader(): ByteArray { + public fun writeHeader(): ByteArray { val buffer = ByteArray(12) writeHeader(buffer.mutableCursor()) return buffer } - fun writeHeader(buffer: MutableByteArrayCursor) = with(buffer) { + public fun writeHeader(buffer: MutableByteArrayCursor): Unit = with(buffer) { resize(cursor + RTP_HEADER_LENGTH) val hasPadding = if (paddingBytes > 0u) 0x20 else 0x00 @@ -131,17 +131,15 @@ data class RTPPacket( writeShort(sequence.toShort()) writeInt(timestamp.toInt()) writeInt(ssrc.toInt()) - - data } - fun asByteArray(): ByteArray { + public fun asByteArray(): ByteArray { val buffer = ByteArray(RTP_HEADER_LENGTH + payload.viewSize + paddingBytes.toInt()) asByteArrayView(buffer.mutableCursor()) return buffer } - fun asByteArrayView(buffer: MutableByteArrayCursor): ByteArrayView = with(buffer) { + public fun asByteArrayView(buffer: MutableByteArrayCursor): ByteArrayView = with(buffer) { resize(cursor + (RTP_HEADER_LENGTH + payload.viewSize + paddingBytes.toInt())) val initial = cursor @@ -160,19 +158,19 @@ data class RTPPacket( data.view(initial, cursor)!! } - class Builder( - var ssrc: UInt, - var timestamp: UInt, - var sequence: UShort, - var payloadType: Byte, - var payload: ByteArray + public class Builder( + public var ssrc: UInt, + public var timestamp: UInt, + public var sequence: UShort, + public var payloadType: Byte, + public var payload: ByteArray ) { - var marker: Boolean = false - var paddingBytes: UByte = 0u - var hasExtension: Boolean = false - var csrcIdentifiers: UIntArray = uintArrayOf() + public var marker: Boolean = false + public var paddingBytes: UByte = 0u + public var hasExtension: Boolean = false + public var csrcIdentifiers: UIntArray = uintArrayOf() - fun build() = RTPPacket( + public fun build(): RTPPacket = RTPPacket( paddingBytes, payloadType, sequence, @@ -186,11 +184,11 @@ data class RTPPacket( } } -fun RTPPacket( +public fun RTPPacket( ssrc: UInt, timestamp: UInt, sequence: UShort, payloadType: Byte, payload: ByteArray, builder: RTPPacket.Builder.() -> Unit = { } -) = RTPPacket.Builder(ssrc, timestamp, sequence, payloadType, payload).apply(builder).build() \ No newline at end of file +): RTPPacket = RTPPacket.Builder(ssrc, timestamp, sequence, payloadType, payload).apply(builder).build() \ No newline at end of file diff --git a/voice/src/main/kotlin/udp/VoiceUdpSocket.kt b/voice/src/main/kotlin/udp/VoiceUdpSocket.kt index d4ec3ed8d67e..c7d6ec52e8c7 100644 --- a/voice/src/main/kotlin/udp/VoiceUdpSocket.kt +++ b/voice/src/main/kotlin/udp/VoiceUdpSocket.kt @@ -9,16 +9,16 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first @KordVoice -interface VoiceUdpSocket { - val incoming: SharedFlow +public interface VoiceUdpSocket { + public val incoming: SharedFlow - suspend fun discoverIp(address: NetworkAddress, ssrc: Int): NetworkAddress + public suspend fun discoverIp(address: NetworkAddress, ssrc: Int): NetworkAddress - suspend fun send(packet: Datagram) + public suspend fun send(packet: Datagram) - suspend fun stop() + public suspend fun stop() - companion object { + public companion object { private object None : VoiceUdpSocket { override val incoming: SharedFlow = MutableSharedFlow() @@ -31,9 +31,10 @@ interface VoiceUdpSocket { override suspend fun stop() {} } - fun none(): VoiceUdpSocket = None + public fun none(): VoiceUdpSocket = None } } @KordVoice -suspend fun VoiceUdpSocket.receiveFrom(address: NetworkAddress) = incoming.filter { it.address == address }.first() \ No newline at end of file +public suspend fun VoiceUdpSocket.receiveFrom(address: NetworkAddress): Datagram = + incoming.filter { it.address == address }.first() \ No newline at end of file