All Downloads are FREE. Search and download functionalities are using the official Maven repository.

main.gateway.VoiceGateway.kt Maven / Gradle / Ivy

package dev.kord.voice.gateway

import dev.kord.common.annotation.KordVoice
import io.ktor.util.logging.*
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

/**
 * An implementation of the Discord [Gateway](https://discord.com/developers/docs/topics/voice-connections) (see gateway-only, not UDP connection in docs) and its lifecycle.
 *
 * Allows consumers to receive [VoiceEvent]s through [events] and send [Command]s through [send].
 */
@KordVoice
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.
     */
    public val events: SharedFlow

    /**
     * The [Duration] between the last [Heartbeat] and [HeartbeatAck].
     *
     * 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.
     */
    public val ping: StateFlow

    /**
     * Starts a reconnection voice gateway connection with the given [configuration]. This function will suspend
     * until the lifecycle of the gateway has ended.
     *
     * @param configuration - the configuration for this gateway session.
     */
    public suspend fun start(configuration: VoiceGatewayConfiguration)

    /**
     * Sends a [Command] to the gateway, suspending until the message has been sent.
     *
     * @param command the [Command] to send to the gateway.
     * @throws Exception when the gateway isn't open/
     */
    public suspend fun send(command: Command)

    /**
     * Close the Gateway and ends the current session, suspending until the underlying websocket is closed.
     */
    public suspend fun stop()

    public companion object {
        private object None : VoiceGateway {
            override val scope: CoroutineScope =
                CoroutineScope(EmptyCoroutineContext + CoroutineName("None VoiceGateway"))

            override val events: SharedFlow
                get() = MutableSharedFlow()

            override val ping: StateFlow
                get() = MutableStateFlow(null)

            override suspend fun send(command: Command) {}

            override suspend fun start(configuration: VoiceGatewayConfiguration) {}

            override suspend fun stop() {}

            override suspend fun detach() {}

            override fun toString(): String {
                return "Gateway.None"
            }
        }

        /**
         * Returns a [VoiceGateway] with no-op behavior, an empty [VoiceGateway.events] flow and a ping of [Duration.ZERO].
         */
        public fun none(): VoiceGateway = None
    }

    public suspend fun detach()
}


/**
 * Logger used to report throwables caught in [VoiceGateway.on].
 */
@PublishedApi
internal val voiceGatewayOnLogger: KLogger = KotlinLogging.logger("Gateway.on")

/**
 * Convenience method that will invoke the [consumer] on every event [T] created by [VoiceGateway.events].
 *
 * The events are buffered in an [unlimited][Channel.UNLIMITED] [buffer][Flow.buffer] and
 * [launched][CoroutineScope.launch] in the supplied [scope], which is [VoiceGateway] by default.
 * Each event will be [launched][CoroutineScope.launch] inside the [scope] separately and
 * any thrown [Throwable] will be caught and logged.
 *
 * The returned [Job] is a reference to the created coroutine, call [Job.cancel] to cancel the processing of any further
 * events for this [consumer].
 */
@KordVoice
public inline fun  VoiceGateway.on(
    scope: CoroutineScope = this.scope,
    crossinline consumer: suspend T.() -> Unit
): Job {
    return this.events.buffer(Channel.UNLIMITED).filterIsInstance().onEach {
        scope.launch { it.runCatching { it.consumer() }.onFailure(voiceGatewayOnLogger::error) }
    }.launchIn(scope)
}

/**
 * Representation of Discord's [Voice Gateway close codes](https://discord.com/developers/docs/topics/opcodes-and-status-codes#voice-voice-close-event-codes).
 */
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
                4003 -> NotAuthenticated
                4004 -> AuthenticationFailed
                4005 -> AlreadyAuthenticated
                4006 -> SessionNoLongerValid
                4009 -> SessionTimeout
                4011 -> ServerNotFound
                4012 -> UnknownProtocol
                4014 -> Disconnect
                4015 -> VoiceServerCrashed
                4016 -> UnknownEncryptionMode
                else -> Unknown(code)
            }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy