Trying to mention the everyone role results in a malformed @@everyone mention in the client

```kotlin
channel.createMessage(guild.everyoneRole.mention)
```

results in:
`@@everyone`

This behaviour on is correct according the official docs, but it seems like the client stumbles over the special everyone type. Using the literal `@everyone` works fine though.

Proposed solution:
Introduce a check on `RoleBehavior#mention` that checks for `guildId.value == id.value` and returns the literal `@everyone` on true instead.

diff --git a/core/src/main/kotlin/com/gitlab/kordlib/core/behavior/RoleBehavior.kt b/core/src/main/kotlin/com/gitlab/kordlib/core/behavior/RoleBehavior.kt
index 01a18c70de..ea9d068296 100644
--- a/core/src/main/kotlin/com/gitlab/kordlib/core/behavior/RoleBehavior.kt
+++ b/core/src/main/kotlin/com/gitlab/kordlib/core/behavior/RoleBehavior.kt
@@ -37,7 +37,11 @@ interface RoleBehavior : Entity, Strategizable {
     /**
      * The raw mention of this entity.
      */
-    val mention: String get() = "<@&${id.value}>"
+    val mention: String
+        get() {
+            return if (guildId.value == id.value) "@everyone"
+            else "<@&${id.value}>"
+        }

     /**
      * Requests to change the [position] of this role.

Trying to mention the everyone role results in a malformed @@everyone mention in the client
```kotlin
channel.createMessage(guild.everyoneRole.mention)
```

results in:
`@@everyone`

This behaviour on is correct according the official docs, but it seems like the client stumbles over the special everyone type. Using the literal `@everyone` works fine though.

Proposed solution:
Introduce a check on `RoleBehavior#mention` that checks for `guildId.value == id.value` and returns the literal `@everyone` on true instead.

import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.event.message.MessageCreateEvent
import com.gitlab.kordlib.core.on

suspend fun main(args: Array<String>) {
    val kord = Kord(args.firstOrNull() ?: error("token required"))

    kord.on<MessageCreateEvent> {
        if (message.author?.isBot == true) return@on
        if (message.content == "!ping") message.channel.createMessage("pong")
    }

    kord.login { playing("!ping to pong") }
}

import com.gitlab.kordlib.common.entity.Activity
import com.gitlab.kordlib.common.entity.ActivityType
import com.gitlab.kordlib.common.entity.Status
import com.gitlab.kordlib.common.ratelimit.BucketRateLimiter
import com.gitlab.kordlib.gateway.DefaultGateway
import com.gitlab.kordlib.gateway.MessageCreate
import com.gitlab.kordlib.gateway.UpdateStatus
import com.gitlab.kordlib.gateway.retry.LinearRetry
import com.gitlab.kordlib.gateway.start
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.features.json.JsonFeature
import io.ktor.client.features.websocket.WebSockets
import io.ktor.util.KtorExperimentalAPI
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.serialization.UnstableDefault
import java.time.Duration
import kotlin.time.ExperimentalTime
import kotlin.time.seconds
import kotlin.time.toKotlinDuration

@FlowPreview
@UnstableDefault
@ExperimentalTime
@ExperimentalCoroutinesApi
@ObsoleteCoroutinesApi
suspend fun main() {
    val token = System.getenv("KORD_TEST_TOKEN")

    val gateway = DefaultGateway {
        url = "wss://gateway.discord.gg/"
        client = HttpClient(CIO) {
            install(WebSockets)
            install(JsonFeature)
        }

        retry = LinearRetry(2.seconds, 20.seconds, 10)
        rateLimiter = BucketRateLimiter(120, Duration.ofSeconds(60).toKotlinDuration())
    }

    gateway.events.filterIsInstance<MessageCreate>().flowOn(Dispatchers.Default).onEach {
        val words = it.message.content.split(' ')
        when (words.firstOrNull()) {
            "!close" -> gateway.stop()
            "!restart" -> gateway.restart()
            "!detach" -> gateway.detach()
            "!status" -> when (words.getOrNull(1)) {
                "playing" -> gateway.send(UpdateStatus(status = Status.Online, afk = false, game = Activity("Kord", ActivityType.Game)))
            }
        }
    }.launchIn(GlobalScope)

    gateway.start(token)
}

package com.gitlab.kordlib.rest

import com.gitlab.kordlib.rest.ratelimit.ExclusionRequestHandler
import com.gitlab.kordlib.rest.service.RestClient

suspend fun main(args: Array<String>) {
    val token = args.firstOrNull() ?: error("token required")

    val client = RestClient(ExclusionRequestHandler(token))

    val username = client.user.getCurrentUser().username
    println("using $username's token")
}

import javassist.CtClass
import me.champeau.gradle.japicmp.JapicmpTask
import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.file.FileCollection
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.options.Option
import org.gradle.jvm.tasks.Jar
import org.gradle.kotlin.dsl.create
import org.gradle.kotlin.dsl.register
import java.io.File
import java.nio.file.Files
import java.nio.file.Paths

open class BinaryCompatibilityExtension {
    var disableRootProject: Boolean = false
    val ignoreProjects: MutableList<String> = mutableListOf()
    val includeProjects: MutableList<String> = mutableListOf()
}

class BinaryCompatibilityPlugin : Plugin<Project> {

    override fun apply(target: Project) {
        val extension = target.extensions.create<BinaryCompatibilityExtension>("binaryCompatibility")

        target.gradle.projectsEvaluated {

            target.tasks.register<ApiDumpTask>("dumpApi") {
                dependsOn("jar")
                configure(extension)
            }

            target.allprojects {
                with(extension) {
                    if (disableRootProject && rootProject == this@allprojects) return@allprojects
                    if (includeProjects.isNotEmpty() && name !in includeProjects) return@allprojects
                    if (ignoreProjects.isNotEmpty() && name in ignoreProjects) return@allprojects
                }

                val printReport = tasks.create("print") {
                    doFirst {
                        val report = file("$buildDir/reports/japi.txt")
                        val text = report.readText()
                        if (text.contains("No changes.")) {
                            return@doFirst
                        }

                        project.logger.error(text)
                    }

                }

                val binaryCompat = tasks.register<JapicmpTask>("binaryCheck") {
                    dependsOn("jar")
                    finalizedBy(printReport)

                    oldClasspath = fetchDumpedApi(this@allprojects)
                    val jar = tasks.getByName("jar") as Jar
                    newClasspath = target.files(jar.archiveFile)

                    isIncludeSynthetic = true
                    isFailOnModification = true
                    isIgnoreMissingClasses = true
                    isOnlyBinaryIncompatibleModified = true
                    txtOutputFile = file("$buildDir/reports/japi.txt")

                    class IgnoreInlinedLambdaFilter : japicmp.filter.ClassFilter {
                        override fun matches(classBinary: CtClass): Boolean {
                            return classBinary.name.contains("inlined")
                        }
                    }

                    class IgnoreGenerated : japicmp.filter.ClassFilter {

                        //ClassName$lowerCaseMethodName$optionalNumber
                        val generatedNestedClass = Regex(pattern = """\w+(\$\p{Lower}\w*)+(\$\d+)*""")
                        override fun matches(classBinary: CtClass): Boolean {
                            return when {
                                classBinary.name.any { it.isDigit() } -> true
                                classBinary.name.matches(generatedNestedClass) -> true
                                else -> false
                            }
                        }
                    }

                    addExcludeFilter(IgnoreInlinedLambdaFilter::class.java)
                    addExcludeFilter(IgnoreGenerated::class.java)

                }

                rootProject.tasks.getByName("check") {
                    dependsOn(binaryCompat)
                }

            }

        }

    }

    fun fetchDumpedApi(project: Project): FileCollection {
        val task = project.tasks.getByName("jar") as Jar
        val jarFile = task.archiveFile.get()

        val destination = Paths.get(project.projectDir.path, "api", jarFile.asFile.name)
        return project.files(destination)
    }

}

open class ApiDumpTask : DefaultTask() {

    @get:Input
    @set:Option(option = "tag", description = """
The version of the API to dump.
Use `$CURRENT_FLAG` to dump the current, local version of the API.
Use `$LATEST_FLAG` to dump the latest published version of the API.
`$LATEST_FLAG` by default.
"""
    )
    var version: String = LATEST_FLAG

    private var acceptProjectsPredicate: Project.() -> Boolean = { true }
    private var denyProjectsPredicate: Project.() -> Boolean = { false }
    private var ignoreRootProject: Boolean = false

    @TaskAction
    fun dumpApi() {
        project.allprojects {
            if (ignoreRootProject && this.rootProject == this) return@allprojects

            if (!acceptProjectsPredicate()) return@allprojects
            if (denyProjectsPredicate()) return@allprojects

            val jarFile = when (this@ApiDumpTask.version) {
                LATEST_FLAG -> {
                    val releaseVersion = getReleaseVersion("+")
                    getPublishedJar(this, releaseVersion)
                }
                CURRENT_FLAG -> {
                    val jarTask = project.tasks.getByName("jar") as Jar
                    jarTask.archiveFile.get().asFile
                }
                else -> {
                    val releaseVersion = getReleaseVersion(this@ApiDumpTask.version)
                    getPublishedJar(this, releaseVersion)
                }
            }

            val destination = Paths.get(project.projectDir.path, "api", "${this.name}.jar")
            Files.deleteIfExists(destination)

            Files.copy(jarFile.toPath(), destination)
        }

    }

    private fun getReleaseVersion(version: String): String {
        /**
         * There's probably a better way to do this, but it works.
         * We create a custom configuration, which we'll delete afterwards, to
         * resolve a dependency on kord-core with the `+` version.
         */
        val configurationName = "resolve-release-version"
        val configuration = project.configurations.create(configurationName)
        val dependency = project.dependencies.create("com.gitlab.kordlib.kord:kord-core:$version")
        project.dependencies.add(configurationName, dependency)

        val lenient = configuration.resolvedConfiguration.lenientConfiguration
        val resolvedDependency = lenient.firstLevelModuleDependencies
                .first { "com.gitlab.kordlib.kord:kord-core:" in it.name }

        project.configurations.remove(configuration)

        return resolvedDependency.moduleVersion
    }

    private fun getPublishedJar(project: Project, version: String): File {
        val oldGroup = project.group
        val group = Library.group
        val artifactId = "kord-${project.name}"

        try {
            val jarFile = "$artifactId-$version.jar"
            project.group = "virtual_group_for_japicmp" // Prevent it from resolving the current version.
            val dependency = project.dependencies.create("$group:$artifactId:$version@jar")
            return project.configurations.detachedConfiguration(dependency).files
                    .find { (it.name == jarFile) } ?: error("$dependency not found")
        } finally {
            project.group = oldGroup
        }
    }

    fun configure(extension: BinaryCompatibilityExtension) {
        ignoreRootProject = extension.disableRootProject

        if (extension.ignoreProjects.isNotEmpty()) {
            denyProjectsPredicate = { name !in extension.ignoreProjects }
        }

        if (extension.includeProjects.isNotEmpty()) {
            acceptProjectsPredicate = { name in extension.includeProjects }
        }
    }

    companion object {
        private const val LATEST_FLAG = "latest"
        private const val CURRENT_FLAG = "current"

    }

}

object CompilerArguments {
    const val inlineClasses = "-XXLanguage:+InlineClasses"
    const val coroutines = "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
    const val time = "-Xopt-in=kotlin.time.ExperimentalTime"
    const val stdLib = "-Xopt-in=kotlin.ExperimentalStdlibApi"
    const val optIn = "-Xopt-in=kotlin.RequiresOptIn"
}

object Jvm {
    const val target = "1.8"
}

object  Versions {
    const val kotlin = "1.4.0"
    const val kotlinxSerialization = "1.0.0-RC"
    const val ktor = "1.4.0"
    const val kotlinxCoroutines = "1.3.9"
    const val kotlinLogging = "1.7.10"
    const val atomicFu = "0.14.4"

    //test deps
    const val kotlinTest = kotlin
    const val junit5 = "5.6.0"
    const val kotlinxCoroutinesTest = kotlinxCoroutines
    const val kotlinReflect = kotlin
    const val junitJupiterApi = junit5
    const val junitJupiterEngine = junit5
    const val sl4j = "1.7.30"
    const val bintray = "1.8.5"
    const val mockk = "1.10.0"
}

@Suppress("ObjectPropertyName")
object Dependencies {
    const val jdk8 = "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
    const val `kotlinx-serialization` = "org.jetbrains.kotlinx:kotlinx-serialization-core:${Versions.kotlinxSerialization}"
    const val `kotlinx-coroutines` = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.kotlinxCoroutines}"
    const val `kotlinx-atomicfu` = ""

    const val `kotlin-logging` = "io.github.microutils:kotlin-logging:${Versions.kotlinLogging}"

    const val `kotlin-test` = "org.jetbrains.kotlin:kotlin-test:${Versions.kotlinTest}"
    const val junit5 = "org.jetbrains.kotlin:kotlin-test-junit5:${Versions.kotlinTest}"
    const val `kotlinx-coroutines-test` = "org.jetbrains.kotlinx:kotlinx-coroutines-test:${Versions.kotlinxCoroutinesTest}"
    const val `kotlin-reflect` = "org.jetbrains.kotlin:kotlin-reflect:${Versions.kotlinReflect}"
    const val `junit-jupiter-api` = "org.junit.jupiter:junit-jupiter-api:${Versions.junitJupiterApi}"
    const val `junit-jupiter-engine` = "org.junit.jupiter:junit-jupiter-engine:${Versions.junitJupiterEngine}"
    const val sl4j = "org.slf4j:slf4j-simple:${Versions.sl4j}"

    const val `ktor-client-json` =  "io.ktor:ktor-client-json:${Versions.ktor}"
    const val `ktor-client-json-jvm` = "io.ktor:ktor-client-json-jvm:${Versions.ktor}"
    const val `ktor-client-serialization-jvm` = "io.ktor:ktor-client-serialization-jvm:${Versions.ktor}"
    const val `ktor-client-cio` = "io.ktor:ktor-client-cio:${Versions.ktor}"
    const val `ktor-client-websocket` = "io.ktor:ktor-client-websockets:${Versions.ktor}"
    const val `ktor-client-mock` = "io.ktor:ktor-client-mock:${Versions.ktor}"
    const val `ktor-client-mock-jvm`= "io.ktor:ktor-client-mock-jvm:${Versions.ktor}"

    const val `cache-api` = "com.gitlab.kordlib.cache:api"
    const val `cache-map` = "com.gitlab.kordlib.cache:map"

    const val mockk = "io.mockk:mockk:${Versions.mockk}"
}

object Plugins {
    const val kapt = "org.jetbrains.kotlin.kapt"
}

import org.gradle.kotlin.dsl.DependencyHandlerScope
import org.gradle.kotlin.dsl.project

val DependencyHandlerScope.common get() = project(":common")
val DependencyHandlerScope.rest get() = project(":rest")
val DependencyHandlerScope.gateway get() = project(":gateway")
val DependencyHandlerScope.core get() = project(":core")

object Library {
    const val group = "com.gitlab.kordlib.kord"
    val version = System.getenv("RELEASE_TAG") ?: System.getenv("GITHUB_SHA") ?: "undefined"
    const val description = "Idiomatic Kotlin Wrapper for The Discord API"
}

import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction
import java.nio.file.Files
import java.nio.file.Paths

open class DocsTask : DefaultTask() {

    @TaskAction
    fun publish() {
        val dokkaRoot = Paths.get(project.projectDir.absolutePath, "dokka")

        val modulesFile = Paths.get(dokkaRoot.toAbsolutePath().toString(), "-modules.html")
        val newIndex = Paths.get(dokkaRoot.toAbsolutePath().toString(), "index.html")

        Files.copy(modulesFile, newIndex)
    }

}

package com.gitlab.kordlib.core

import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.gateway.Intents
import io.ktor.client.HttpClient

class ClientResources(
        val token: String,
        val shardCount: Int,
        val httpClient: HttpClient,
        val defaultStrategy: EntitySupplyStrategy<*>,
        val intents: Intents?
)

package com.gitlab.kordlib.core

import com.gitlab.kordlib.cache.api.DataCache
import com.gitlab.kordlib.common.annotation.KordExperimental
import com.gitlab.kordlib.common.annotation.KordUnsafe
import com.gitlab.kordlib.common.entity.DiscordShard
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.entity.Status
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.builder.kord.KordBuilder
import com.gitlab.kordlib.core.builder.kord.KordRestOnlyBuilder
import com.gitlab.kordlib.core.cache.data.GuildData
import com.gitlab.kordlib.core.entity.ApplicationInfo
import com.gitlab.kordlib.core.entity.Guild
import com.gitlab.kordlib.core.entity.Region
import com.gitlab.kordlib.core.entity.User
import com.gitlab.kordlib.core.entity.channel.Channel
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.exception.KordInitializationException
import com.gitlab.kordlib.core.gateway.MasterGateway
import com.gitlab.kordlib.core.gateway.handler.GatewayEventInterceptor
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.core.supplier.getChannelOfOrNull
import com.gitlab.kordlib.gateway.Gateway
import com.gitlab.kordlib.gateway.builder.PresenceBuilder
import com.gitlab.kordlib.rest.builder.guild.GuildCreateBuilder
import com.gitlab.kordlib.rest.service.RestClient
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import mu.KotlinLogging
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.channels.Channel as CoroutineChannel

val kordLogger = KotlinLogging.logger { }

class Kord(
        val resources: ClientResources,
        val cache: DataCache,
        val gateway: MasterGateway,
        val rest: RestClient,
        val selfId: Snowflake,
        private val eventPublisher: BroadcastChannel<Event>,
        private val dispatcher: CoroutineDispatcher,
) : CoroutineScope {
    private val interceptor = GatewayEventInterceptor(this, gateway, cache, eventPublisher)

    init {
        launch { interceptor.start() }
    }

    val defaultSupplier: EntitySupplier = resources.defaultStrategy.supply(this)

    @OptIn(KordUnsafe::class)
    val unsafe: Unsafe = Unsafe(this)

    val events get() = eventPublisher.asFlow().buffer(CoroutineChannel.UNLIMITED)

    override val coroutineContext: CoroutineContext
        get() = dispatcher + Job()

    val regions: Flow<Region>
        get() = resources.defaultStrategy.supply(this).regions

    val guilds: Flow<Guild>
        get() = resources.defaultStrategy.supply(this).guilds

    /**
     * Logs in to the configured [Gateways][Gateway]. Suspends until [logout] or [shutdown] is called.
     */
    @OptIn(ExperimentalContracts::class)
    suspend inline fun login(builder: PresenceBuilder.() -> Unit = { status = Status.Online }) {
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }
        gateway.start(resources.token) {
            shard = DiscordShard(0, resources.shardCount)
            presence(builder)
            intents = resources.intents
            name = "kord"
        }
    }

    /**
     * Logs out to the configured [Gateways][Gateway].
     */
    suspend fun logout() = gateway.stopAll()

    /**
     * Logs out of all connected [Gateways][Gateway] and frees all resources.
     */
    suspend fun shutdown() {
        gateway.detachAll()
        this.eventPublisher.close()
    }

    fun <T : EntitySupplier> with(strategy: EntitySupplyStrategy<T>): T = strategy.supply(this)

    suspend fun getApplicationInfo(): ApplicationInfo = with(EntitySupplyStrategy.rest).getApplicationInfo()

    @OptIn(ExperimentalContracts::class)
    suspend inline fun createGuild(builder: GuildCreateBuilder.() -> Unit): Guild {
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }
        val response = rest.guild.createGuild(builder)
        val data = GuildData.from(response)

        return Guild(data, this)
    }

    suspend fun getChannel(id: Snowflake, strategy: EntitySupplyStrategy<*> = resources.defaultStrategy): Channel? = strategy.supply(this).getChannelOrNull(id)

    /**
     * Requests to get the [Channel] as type [T] through the [strategy],
     * returns null if the [Channel] isn't present or is not of type [T].
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend inline fun <reified T : Channel> getChannelOf(
            id: Snowflake,
            strategy: EntitySupplyStrategy<*> = resources.defaultStrategy,
    ): T? = strategy.supply(this).getChannelOfOrNull(id)

    suspend fun getGuild(id: Snowflake, strategy: EntitySupplyStrategy<*> = resources.defaultStrategy): Guild? = strategy.supply(this).getGuildOrNull(id)

    suspend fun getSelf(strategy: EntitySupplyStrategy<*> = resources.defaultStrategy): User =
            strategy.supply(this).getSelf()

    suspend fun getUser(id: Snowflake, strategy: EntitySupplyStrategy<*> = resources.defaultStrategy): User? =
            strategy.supply(this).getUserOrNull(id)

    @OptIn(ExperimentalContracts::class)
    suspend inline fun editPresence(builder: PresenceBuilder.() -> Unit) {
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }
        val status = PresenceBuilder().apply(builder).toUpdateStatus()
        gateway.sendAll(status)
    }

    override fun equals(other: Any?): Boolean {
        val kord = other as? Kord ?: return false

        return resources.token == kord.resources.token
    }

    companion object {

        /**
         * Builds a [Kord] instance configured by the [builder].
         *
         * @throws KordInitializationException if something went wrong while getting the bot's gateway information.
         */
        suspend inline operator fun invoke(token: String, builder: KordBuilder.() -> Unit = {}): Kord =
                KordBuilder(token).apply(builder).build()

        /**
         * Builds a [Kord] instance configured by the [builder].
         *
         * The instance only allows for configuration of REST related APIs,
         * interacting with the [gateway][Kord.gateway] or its [events][Kord.events] will result in no-ops.
         *
         * Similarly, [cache][Kord.cache] related functionality has been disabled and
         * replaced with a no-op implementation.
         */
        @OptIn(ExperimentalContracts::class)
        @KordExperimental
        inline fun restOnly(token: String, builder: KordRestOnlyBuilder.() -> Unit = {}): Kord {
            contract {
                callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
            }
            return KordRestOnlyBuilder(token).apply(builder).build()
        }
    }

}

/**
 * Convenience method that will invoke the [consumer] on every event [T] created by [Kord.events].
 *
 * The events are buffered in an [unlimited][CoroutineChannel.UNLIMITED] [buffer][Flow.buffer] and
 * [launched][CoroutineScope.launch] in the supplied [scope], which is [Kord] 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.
 */
inline fun <reified T : Event> Kord.on(scope: CoroutineScope = this, noinline consumer: suspend T.() -> Unit): Job =
        events.buffer(CoroutineChannel.UNLIMITED).filterIsInstance<T>()
                .onEach {
                    scope.launch { runCatching { consumer(it) }.onFailure { kordLogger.catching(it) } }
                }
                .launchIn(scope)

package com.gitlab.kordlib.core

/**
 * An instance than contains a reference to [Kord].
 */
interface KordObject {

    /**
     * The kord instance that created this object.
     */
    val kord: Kord
}

package com.gitlab.kordlib.core

import com.gitlab.kordlib.common.annotation.KordExperimental
import com.gitlab.kordlib.common.annotation.KordUnsafe
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.behavior.*
import com.gitlab.kordlib.core.behavior.channel.*

/**
 * A class that exposes the creation of `{Entity}Behavior` classes.
 *
 * All functionality in this class *assumes* correct data is being passed along
 * and omits any requirements or checks. This makes using behaviors created by this
 * class inherently unsafe.
 *
 * If the user is not sure of the correctness of the data being passed along, it is advised
 * to use [Entities][com.gitlab.kordlib.core.entity.Entity] generated by [Kord] or other Entities instead.
 */
@KordUnsafe
@KordExperimental
@Suppress("EXPERIMENTAL_API_USAGE")
class Unsafe(private val kord: Kord) {

    fun message(channelId: Snowflake, messageId: Snowflake): MessageBehavior =
            MessageBehavior(channelId = channelId, messageId = messageId, kord = kord)

    fun channel(id: Snowflake): ChannelBehavior =
            ChannelBehavior(id, kord)

    fun messageChannel(id: Snowflake): MessageChannelBehavior =
            MessageChannelBehavior(id, kord)

    fun guildChannel(guildId: Snowflake, id: Snowflake): GuildChannelBehavior =
            GuildChannelBehavior(guildId = guildId, id = id, kord = kord)

    fun guildMessageChannel(guildId: Snowflake, id: Snowflake): GuildMessageChannelBehavior =
            GuildMessageChannelBehavior(guildId = guildId, id = id, kord = kord)

    fun newsChannel(guildId: Snowflake, id: Snowflake): NewsChannelBehavior =
            NewsChannelBehavior(guildId = guildId, id = id, kord = kord)

    fun textChannel(guildId: Snowflake, id: Snowflake): TextChannelBehavior =
            TextChannelBehavior(guildId = guildId, id = id, kord = kord)

    fun voiceChannel(guildId: Snowflake, id: Snowflake): VoiceChannelBehavior =
            VoiceChannelBehavior(guildId = guildId, id = id, kord = kord)

    fun storeChannel(guildId: Snowflake, id: Snowflake): StoreChannelBehavior =
            StoreChannelBehavior(guildId = guildId, id = id, kord = kord)

    fun guild(id: Snowflake): GuildBehavior =
            GuildBehavior(id, kord)

    fun guildEmoji(guildId: Snowflake, id: Snowflake, kord: Kord): GuildEmojiBehavior =
            GuildEmojiBehavior(guildId = guildId, id = id, kord = kord)

    fun role(guildId: Snowflake, id: Snowflake): RoleBehavior =
            RoleBehavior(guildId = guildId, id = id, kord = kord)

    fun user(id: Snowflake): UserBehavior =
            UserBehavior(id, kord)

    fun member(guildId: Snowflake, id: Snowflake): MemberBehavior =
            MemberBehavior(guildId = guildId, id = id, kord = kord)

    fun webhook(id: Snowflake): WebhookBehavior =
            WebhookBehavior(id, kord)

}

package com.gitlab.kordlib.core

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.entity.Entity
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.event.PresenceUpdateEvent
import com.gitlab.kordlib.core.event.VoiceStateUpdateEvent
import com.gitlab.kordlib.core.event.WebhookUpdateEvent
import com.gitlab.kordlib.core.event.channel.*
import com.gitlab.kordlib.core.event.guild.*
import com.gitlab.kordlib.core.event.message.*
import com.gitlab.kordlib.core.event.role.RoleCreateEvent
import com.gitlab.kordlib.core.event.role.RoleDeleteEvent
import com.gitlab.kordlib.core.event.role.RoleUpdateEvent
import com.gitlab.kordlib.gateway.Intent.*
import com.gitlab.kordlib.gateway.Intents
import com.gitlab.kordlib.gateway.MessageDelete
import com.gitlab.kordlib.gateway.PrivilegedIntent
import com.gitlab.kordlib.rest.request.RestRequestException
import com.gitlab.kordlib.rest.route.Position
import kotlinx.coroutines.flow.*
import java.time.Instant
import java.time.format.DateTimeFormatter
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.reflect.KClass

internal fun String?.toSnowflakeOrNull(): Snowflake? = when {
    this == null -> null
    else -> Snowflake(this)
}

internal fun Long?.toSnowflakeOrNull(): Snowflake? = when {
    this == null -> null
    else -> Snowflake(this)
}

internal fun String.toInstant() = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(this, Instant::from)
internal fun Int.toInstant() = Instant.ofEpochMilli(toLong())
internal fun Long.toInstant() = Instant.ofEpochMilli(this)

@OptIn(ExperimentalContracts::class)
internal inline fun <T> catchNotFound(block: () -> T): T?  {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return try {
        block()
    } catch (exception: RestRequestException) {
        if (exception.code == 404) null
        else throw exception
    }
}

fun <T : Entity> Flow<T>.sorted(): Flow<T> = flow {
    for (entity in toList().sorted()) {
        emit(entity)
    }
}

/**
 * The terminal operator that returns the first element emitted by the flow that matches the [predicate]
 * and then cancels flow's collection.
 * Returns `null` if the flow was empty.
 */
suspend inline fun <T : Any> Flow<T>.firstOrNull(crossinline predicate: suspend (T) -> Boolean): T? =
        filter { predicate(it) }.firstOrNull()

/**
 * The terminal operator that returns `true` if any of the elements match [predicate].
 * The flow's collection is cancelled when a match is found.
 */
suspend inline fun <T : Any> Flow<T>.any(crossinline predicate: suspend (T) -> Boolean): Boolean =
        firstOrNull(predicate) != null

/**
 * The non-terminal operator that returns a new flow that will emit values of the second [flow] only after the first
 * flow finished collecting without values.
 *
 * ```kotlin
 * emptyFlow<String>().switchIfEmpty(flowOf("hello", "world")) //["hello", "world"]
 *
 * flowOf("hello", "world").switchIfEmpty(flowOf("goodbye", "world")) //["hello", "world"]
 * ```
 */
internal fun <T> Flow<T>.switchIfEmpty(flow: Flow<T>): Flow<T> = flow {
    var empty = true
    collect {
        empty = false
        emit(it)
    }

    if (empty) {
        flow.collect {
            emit(it)
        }
    }
}

/**
 * The terminal operator that returns the index of the first element emitted by the flow that matches the [predicate]
 * and then cancels flow's collection.
 * Returns `null` if the flow was empty or no element matched the [predicate].
 */
internal suspend fun <T> Flow<T>.indexOfFirstOrNull(predicate: suspend (T) -> Boolean): Int? {
    var counter = 0
    return map {
        val pair = counter to it
        counter += 1
        pair
    }
            .filter { predicate(it.second) }
            .take(1)
            .singleOrNull()?.first
}

internal fun <C : Collection<T>, T> paginate(
        start: String,
        batchSize: Int,
        idSelector: (T) -> String,
        itemSelector: (Collection<T>) -> T?,
        directionSelector: (String) -> Position,
        request: suspend (position: Position) -> C,
): Flow<T> = flow {
    var position = directionSelector(start)
    var size = batchSize

    while (true) {
        val response = request(position)
        for (item in response) emit(item)

        val id = itemSelector(response)?.let(idSelector) ?: break
        position = directionSelector(id)

        if (response.size < size) break
        size = response.size
    }
}

/**
 * Discord returns values in order newest -> oldest (big -> small) (confirmed for messages),
 * meaning that the first item returned is the one last created (youngest) in the batch.
 */
internal fun <T> youngestItem(idSelector: (T) -> String): (Collection<T>) -> T? = function@{
    if (it.size <= 1) return@function it.firstOrNull()

    val first = it.first()
    val last = it.last()

    val firstId = idSelector(first).toLong()
    val lastId = idSelector(last).toLong()

    if (firstId > lastId) first
    else last
}

/**
 * Discord returns values in order oldest -> newest (big -> small) (confirmed for messages),
 * meaning that the last item returned is the one first created (oldest) in the batch.
 */
internal fun <T> oldestItem(idSelector: (T) -> String): (Collection<T>) -> T? = function@{
    if (it.size <= 1) return@function it.firstOrNull()
    val first = it.first()
    val last = it.last()

    val firstId = idSelector(first).toLong()
    val lastId = idSelector(last).toLong()

    if (firstId < lastId) first
    else last
}

/**
 *  Selects the [Position.After] the youngest item in the batch.
 */
internal fun <C : Collection<T>, T> paginateForwards(start: Snowflake = Snowflake("0"), batchSize: Int, idSelector: (T) -> String, request: suspend (position: Position) -> C): Flow<T> =
        paginate(start.value, batchSize, idSelector, youngestItem(idSelector), Position::After, request)

/**
 *  Selects the [Position.After] the youngest item in the batch.
 */
internal fun <C : Collection<T>, T : Entity> paginateForwards(start: Snowflake = Snowflake("0"), batchSize: Int, request: suspend (position: Position) -> C): Flow<T> =
        paginate(start.value, batchSize, { it.id.value }, youngestItem { it.id.value }, Position::After, request)

/**
 *  Selects the [Position.Before] the oldest item in the batch.
 */
internal fun <C : Collection<T>, T> paginateBackwards(start: Snowflake = Snowflake(Long.MAX_VALUE), batchSize: Int, idSelector: (T) -> String, request: suspend (position: Position) -> C): Flow<T> =
        paginate(start.value, batchSize, idSelector, oldestItem(idSelector), Position::Before, request)

/**
 *  Selects the [Position.Before] the oldest item in the batch.
 */
internal fun <C : Collection<T>, T : Entity> paginateBackwards(start: Snowflake = Snowflake(Long.MAX_VALUE), batchSize: Int, request: suspend (position: Position) -> C): Flow<T> =
        paginate(start.value, batchSize, { it.id.value }, oldestItem { it.id.value }, Position::Before, request)

inline fun <reified T : Event> Intents.IntentsBuilder.enableEvent() = enableEvent(T::class)

fun Intents.IntentsBuilder.enableEvents(events: Iterable<KClass<out Event>>) = events.forEach { enableEvent(it) }
fun Intents.IntentsBuilder.enableEvents(vararg events: KClass<out Event>) = events.forEach { enableEvent(it) }

@OptIn(PrivilegedIntent::class)
fun Intents.IntentsBuilder.enableEvent(event: KClass<out Event>) = when (event) {
    GuildCreateEvent::class,
    GuildDeleteEvent::class,
    RoleCreateEvent::class,
    RoleUpdateEvent::class,
    RoleDeleteEvent::class,

    ChannelCreateEvent::class,
    CategoryCreateEvent::class,
    DMChannelCreateEvent::class,
    NewsChannelCreateEvent::class,
    StoreChannelCreateEvent::class,
    TextChannelCreateEvent::class,
    VoiceChannelCreateEvent::class,

    ChannelUpdateEvent::class,
    CategoryUpdateEvent::class,
    DMChannelUpdateEvent::class,
    NewsChannelUpdateEvent::class,
    StoreChannelUpdateEvent::class,
    TextChannelUpdateEvent::class,
    VoiceChannelUpdateEvent::class,

    ChannelDeleteEvent::class,
    CategoryDeleteEvent::class,
    DMChannelDeleteEvent::class,
    NewsChannelDeleteEvent::class,
    StoreChannelDeleteEvent::class,
    TextChannelDeleteEvent::class,
    VoiceChannelDeleteEvent::class,

    ChannelPinsUpdateEvent::class,
    -> {
        +Guilds
        +DirectMessages
    }

    MemberJoinEvent::class, MemberUpdateEvent::class, MemberLeaveEvent::class -> +GuildMembers

    BanAddEvent::class, BanRemoveEvent::class -> +GuildBans

    EmojisUpdateEvent::class -> +GuildEmojis

    IntegrationsUpdateEvent::class -> +GuildIntegrations

    WebhookUpdateEvent::class -> +GuildWebhooks

    InviteCreateEvent::class, InviteDeleteEvent::class -> +GuildInvites

    VoiceStateUpdateEvent::class -> +GuildVoiceStates

    PresenceUpdateEvent::class -> +GuildPresences

    MessageCreateEvent::class, MessageUpdateEvent::class, MessageDelete::class -> {
        +GuildMessages
        +DirectMessages
    }

    ReactionAddEvent::class, ReactionRemoveEvent::class, ReactionRemoveAllEvent::class, ReactionRemoveEmojiEvent::class -> {
        +GuildMessageReactions
        +DirectMessagesReactions
    }

    TypingStartEvent::class -> +GuildMessageTyping

    else -> Unit

}

package com.gitlab.kordlib.gateway

import com.gitlab.kordlib.common.entity.*
import kotlinx.serialization.*
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonObject

sealed class Command {
    internal data class Heartbeat(val sequenceNumber: Int? = null) : Command() {

        companion object : SerializationStrategy<Heartbeat> {
            override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Heartbeat", PrimitiveKind.INT)

            @OptIn(ExperimentalSerializationApi::class)
            override fun serialize(encoder: Encoder, obj: Heartbeat) {
                encoder.encodeNullableSerializableValue(Int.serializer(), obj.sequenceNumber)
            }
        }

    }

    companion object : SerializationStrategy<Command> {

        override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Command") {
            element("op", OpCode.descriptor)
            element("d", JsonObject.serializer().descriptor)
        }

        override fun serialize(encoder: Encoder, obj: Command) {
            val composite = encoder.beginStructure(descriptor)
            when (obj) {
                is RequestGuildMembers -> {
                    composite.encodeSerializableElement(descriptor, 0, OpCode, OpCode.RequestGuildMembers)
                    composite.encodeSerializableElement(descriptor, 1, RequestGuildMembers.serializer(), obj)
                }
                is UpdateVoiceStatus -> {
                    composite.encodeSerializableElement(descriptor, 0, OpCode, OpCode.VoiceStateUpdate)
                    composite.encodeSerializableElement(descriptor, 1, UpdateVoiceStatus.serializer(), obj)
                }
                is UpdateStatus -> {
                    composite.encodeSerializableElement(descriptor, 0, OpCode, OpCode.StatusUpdate)
                    composite.encodeSerializableElement(descriptor, 1, UpdateStatus.serializer(), obj)
                }
                is Identify -> {
                    composite.encodeSerializableElement(descriptor, 0, OpCode, OpCode.Identify)
                    composite.encodeSerializableElement(descriptor, 1, Identify.serializer(), obj)
                }
                is Resume -> {
                    composite.encodeSerializableElement(descriptor, 0, OpCode, OpCode.Resume)
                    composite.encodeSerializableElement(descriptor, 1, Resume.serializer(), obj)
                }
                is Heartbeat -> {
                    composite.encodeSerializableElement(descriptor, 0, OpCode, OpCode.Heartbeat)
                    composite.encodeSerializableElement(descriptor, 1, Heartbeat.Companion, obj)
                }
            }

            composite.endStructure(descriptor)
        }

    }

}

@Serializable
internal data class Identify(
        internal val token: String,
        val properties: IdentifyProperties,
        val compress: Boolean? = null,
        @SerialName("large_threshold")
        val largeThreshold: Int = 50,
        val shard: DiscordShard? = null,
        val presence: Presence? = null,
        val intents: Intents? = null
) : Command() {
    override fun toString(): String = "Identify(token=hunter2,properties=$properties,compress=$compress,largeThreshold=$largeThreshold," +
            "shard=$shard,presence=$presence"
}

@Serializable
data class IdentifyProperties(
        @Required
        @SerialName("\$os")
        val os: String,
        @SerialName("\$browser")
        val browser: String,
        @SerialName("\$device")
        val device: String
)

@Serializable
data class GuildMembersChunkData(
        @SerialName("guild_id")
        val guildId: String,
        val members: List<DiscordGuildMember>,
        @SerialName("not_found")
        val notFound: List<String>? = null,
        val presences: List<DiscordPresenceUpdateData>? = null,
        /**
         * The chunk index in the expected chunks for this response.
         */
        @SerialName("chunk_index")
        val chunkIndex: Int,
        /**
         * The total number of expected chunks for this response.
         */
        @SerialName("chunk_count")
        val chunkCount: Int
)

@Serializable
data class Presence(
        val status: Status,
        val afk: Boolean,
        val since: Long? = null,
        val game: DiscordActivity? = null
)

@Serializable
internal data class Resume(
        val token: String,
        @SerialName("session_id")
        val sessionId: String,
        @SerialName("seq")
        val sequenceNumber: Int?
) : Command() {
    override fun toString(): String = "Resume(token=hunter2,sessionId=$sessionId,sequenceNumber:$sequenceNumber)"
}

@Serializable
data class RequestGuildMembers(
        @SerialName("guild_id")
        val guildId: List<String>,
        val query: String = "",
        val limit: Int = 0,
        val presences: Boolean? = null,
        @SerialName("user_ids")
        val userIds: List<String>? = null
) : Command()

@Serializable
data class UpdateVoiceStatus(
        @SerialName("guild_id")
        val guildId: String,
        @SerialName("channel_id")
        val channelId: String? = null,
        @SerialName("self_mute")
        val selfMute: Boolean,
        @SerialName("self_deaf")
        val selfDeaf: Boolean
) : Command()

@Serializable
data class UpdateStatus(
        val since: Long? = null,
        val game: DiscordActivity? = null,
        val status: Status,
        val afk: Boolean
) : Command()

package com.gitlab.kordlib.gateway

import com.gitlab.kordlib.common.ratelimit.RateLimiter
import com.gitlab.kordlib.gateway.GatewayCloseCode.*
import com.gitlab.kordlib.gateway.handler.*
import com.gitlab.kordlib.gateway.retry.Retry
import io.ktor.client.HttpClient
import io.ktor.client.features.websocket.DefaultClientWebSocketSession
import io.ktor.client.features.websocket.webSocketSession
import io.ktor.client.request.url
import io.ktor.http.URLBuilder
import io.ktor.http.cio.websocket.CloseReason
import io.ktor.http.cio.websocket.Frame
import io.ktor.http.cio.websocket.close
import io.ktor.util.error
import kotlinx.atomicfu.AtomicRef
import kotlinx.atomicfu.atomic
import kotlinx.atomicfu.update
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.sendBlocking
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.parse
import kotlinx.serialization.stringify
import mu.KotlinLogging
import java.io.ByteArrayOutputStream
import java.nio.charset.Charset
import java.util.zip.Inflater
import java.util.zip.InflaterOutputStream
import kotlin.time.Duration

private val defaultGatewayLogger = KotlinLogging.logger { }

private sealed class State(val retry: Boolean) {
    object Stopped : State(false)
    class Running(retry: Boolean) : State(retry)
    object Detached : State(false)
}

/**
 * @param url The url to connect to.
 * @param client The client from which a webSocket will be created, requires the WebSockets and JsonFeature to be
 * installed.
 * @param reconnectRetry A retry used for reconnection attempts.
 * @param sendRateLimiter A rate limiter than follows the Discord API specifications for sending messages.
 * @param identifyRateLimiter: A rate limiter that follows the Discord API specifications for identifying.
 */
data class DefaultGatewayData(
        val url: String,
        val client: HttpClient,
        val reconnectRetry: Retry,
        val sendRateLimiter: RateLimiter,
        val identifyRateLimiter: RateLimiter
)

/**
 * The default Gateway implementation of Kord, using an [HttpClient] for the underlying webSocket
 */
@ObsoleteCoroutinesApi
class DefaultGateway(private val data: DefaultGatewayData) : Gateway {

    private val compression: Boolean = URLBuilder(data.url).parameters.contains("compress", "zlib-stream")

    private val channel = BroadcastChannel<Event>(1)

    override var ping: Duration = Duration.INFINITE

    @OptIn(FlowPreview::class)
    override val events: Flow<Event> = channel.asFlow().buffer(Channel.UNLIMITED).filterIsInstance()

    private lateinit var socket: DefaultClientWebSocketSession

    private val state: AtomicRef<State> = atomic(State.Stopped)

    private val handshakeHandler: HandshakeHandler

    private lateinit var inflater: Inflater

    private val jsonParser = Json {
            isLenient = true
            ignoreUnknownKeys = true
            allowSpecialFloatingPointValues = true
            useArrayPolymorphism = true
    }

    private val stateMutex = Mutex()

    init {
        val sequence = Sequence()
        SequenceHandler(events, sequence)
        handshakeHandler = HandshakeHandler(events, ::trySend, sequence, data.identifyRateLimiter)
        HeartbeatHandler(events, ::trySend, { restart(Close.ZombieConnection) }, { ping = it }, sequence)
        ReconnectHandler(events) { restart(Close.Reconnecting) }
        InvalidSessionHandler(events) { restart(it) }
    }

    //running on default dispatchers because ktor does *not* like running on an EmptyCoroutineContext from main
    override suspend fun start(configuration: GatewayConfiguration): Unit = withContext(Dispatchers.Default) {
        resetState(configuration)

        while (data.reconnectRetry.hasNext && state.value is State.Running) {
            try {
                socket = webSocket(data.url)
                /**
                 * https://discord.com/developers/docs/topics/gateway#transport-compression
                 *
                 * > Every connection to the gateway should use its own unique zlib context.
                 */
                inflater = Inflater()
            } catch (exception: Exception) {
                defaultGatewayLogger.error(exception)
                if (exception is java.nio.channels.UnresolvedAddressException) {
                    channel.send(Close.Timeout)
                }

                data.reconnectRetry.retry()
                continue //can't handle a close code if you've got no socket
            }

            try {
                readSocket()
                data.reconnectRetry.reset() //connected and read without problems, resetting retry counter
            } catch (exception: Exception) {
                defaultGatewayLogger.error(exception)
            }

            defaultGatewayLogger.trace { "gateway connection closing" }

            try {
                handleClose()
            } catch (exception: Exception) {
                defaultGatewayLogger.error(exception)
            }

            defaultGatewayLogger.trace { "handled gateway connection closed" }

            if (state.value.retry) data.reconnectRetry.retry()
            else channel.send(Close.RetryLimitReached)
        }

        if (!data.reconnectRetry.hasNext) {
            defaultGatewayLogger.warn { "retry limit exceeded, gateway closing" }
        }
    }

    private suspend fun resetState(configuration: GatewayConfiguration) = stateMutex.withLock {
        @Suppress("UNUSED_VARIABLE")
        val exhaustive = when (state.value) { //exhaustive state checking
            is State.Running -> throw IllegalStateException(gatewayRunningError)
            State.Detached -> throw IllegalStateException(gatewayDetachedError)
            State.Stopped -> Unit
        }

        handshakeHandler.configuration = configuration
        data.reconnectRetry.reset()
        state.update { State.Running(true) } //resetting state
    }

    private suspend fun readSocket() {
        socket.incoming.asFlow().collect {
            when (it) {
                is Frame.Binary, is Frame.Text -> read(it)
                else -> { /*ignore*/
                }
            }
        }

    }

    private fun Frame.deflateData(): String {
        val outputStream = ByteArrayOutputStream()
        InflaterOutputStream(outputStream, inflater).use {
            it.write(data)
        }

        return outputStream.use {
            String(outputStream.toByteArray(), 0, outputStream.size(), Charsets.UTF_8)
        }
    }

    private suspend fun read(frame: Frame) {
        val json = when {
            compression -> frame.deflateData()
            else -> String(frame.data, Charsets.UTF_8)
        }

        try {
            defaultGatewayLogger.trace { "Gateway <<< $json" }
            jsonParser.decodeFromString(Event.Companion, json)?.let { channel.send(it) }
        } catch (exception: Exception) {
            defaultGatewayLogger.error(exception)
        }

    }

    private suspend fun handleClose() {
        val reason = withTimeoutOrNull(1500) {
            socket.closeReason.await()
        } ?: return

        defaultGatewayLogger.trace { "Gateway closed: ${reason.code} ${reason.message}" }
        val discordReason = values().firstOrNull { it.code == reason.code.toInt() } ?: return

        channel.send(Close.DiscordClose(discordReason, discordReason.retry))

        when {
            !discordReason.retry -> {
                state.update { State.Stopped }
                throw  IllegalStateException("Gateway closed: ${reason.code} ${reason.message}")
            }
            discordReason.resetSession -> {
                state.update { State.Running(true) }
            }
        }
    }

    private fun <T> ReceiveChannel<T>.asFlow() = flow {
        try {
            for (value in this@asFlow) emit(value)
        } catch (ignore: CancellationException) {
            //reading was stopped from somewhere else, ignore
        }
    }

    private suspend fun webSocket(url: String) = data.client.webSocketSession { url(url) }

    override suspend fun stop() {
        check(state.value !is State.Detached) { "The resources of this gateway are detached, create another one" }
        channel.send(Close.UserClose)
        state.update { State.Stopped }
        if (socketOpen) socket.close(CloseReason(1000, "leaving"))
    }

    internal suspend fun restart(code: Close) {
        check(state.value !is State.Detached) { "The resources of this gateway are detached, create another one" }
        state.update { State.Running(false) }
        if (socketOpen) {
            channel.send(code)
            socket.close(CloseReason(4900, "reconnecting"))
        }
    }

    override suspend fun detach() {
        if (state.value is State.Detached) return
        state.update { State.Detached }
        channel.send(Close.Detach)
        if (::socket.isInitialized) {
            socket.close()
        }
        channel.cancel()
    }

    override suspend fun send(command: Command) = stateMutex.withLock {
        check(state.value !is State.Detached) { "The resources of this gateway are detached, create another one" }
        sendUnsafe(command)
    }

    private suspend fun trySend(command: Command) = stateMutex.withLock {
        if (state.value !is State.Running) return@withLock
        sendUnsafe(command)
    }

    @Suppress("EXPERIMENTAL_API_USAGE")
    private suspend fun sendUnsafe(command: Command) {
        data.sendRateLimiter.consume()
        val json = Json.encodeToString(Command.Companion, command)
        if (command is Identify) {
            defaultGatewayLogger.trace {
                val copy = command.copy(token = "token")
                "Gateway >>> ${Json.encodeToString(Command.Companion, copy)}"
            }
        }
        else defaultGatewayLogger.trace { "Gateway >>> $json" }
        socket.send(Frame.Text(json))
    }

    private val socketOpen get() = ::socket.isInitialized && !socket.outgoing.isClosedForSend && !socket.incoming.isClosedForReceive

    companion object {

        inline operator fun invoke(builder: DefaultGatewayBuilder.() -> Unit = {}): DefaultGateway =
                DefaultGatewayBuilder().apply(builder).build()

        private const val gatewayRunningError = "The Gateway is already running, call stop() first."
        private const val gatewayDetachedError = "The Gateway has been detached and can no longer be used, create a new instance instead."
    }
}

internal val GatewayConfiguration.identify get() = Identify(token, IdentifyProperties(os, name, name), false, 50, shard, presence, intents)

internal val os: String get() = System.getProperty("os.name")

internal val GatewayCloseCode.retry
    get() = when (this) { //this statement is intentionally structured to ensure we consider the retry for every new code
        Unknown -> true
        UnknownOpCode -> true
        DecodeError -> true
        NotAuthenticated -> true
        AuthenticationFailed -> false
        AlreadyAuthenticated -> true
        InvalidSeq -> true
        RateLimited -> true
        SessionTimeout -> true
        InvalidShard -> false
        ShardingRequired -> false
        InvalidApiVersion -> false
        InvalidIntents -> false
        DisallowedIntents -> false
    }

internal val GatewayCloseCode.resetSession
    get() = when (this) { //this statement is intentionally structured to ensure we consider the reset for every new code
        Unknown -> false
        UnknownOpCode -> false
        DecodeError -> false
        NotAuthenticated -> false
        AuthenticationFailed -> false
        AlreadyAuthenticated -> false
        InvalidSeq -> true
        RateLimited -> false
        SessionTimeout -> false
        InvalidShard -> false
        ShardingRequired -> false
        InvalidApiVersion -> false
        InvalidIntents -> false
        DisallowedIntents -> false
    }

package com.gitlab.kordlib.gateway

import com.gitlab.kordlib.common.ratelimit.BucketRateLimiter
import com.gitlab.kordlib.common.ratelimit.RateLimiter
import com.gitlab.kordlib.gateway.retry.LinearRetry
import com.gitlab.kordlib.gateway.retry.Retry
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.features.json.JsonFeature
import io.ktor.client.features.websocket.WebSockets
import io.ktor.util.*
import kotlinx.coroutines.ObsoleteCoroutinesApi
import java.time.Duration
import kotlin.time.seconds
import kotlin.time.toKotlinDuration

class DefaultGatewayBuilder {
    var url = "wss://gateway.discord.gg/?v=6&encoding=json&compress=zlib-stream"
    var client: HttpClient? = null
    var reconnectRetry: Retry? = null
    var sendRateLimiter: RateLimiter? = null
    var identifyRateLimiter: RateLimiter? = null

    @OptIn(KtorExperimentalAPI::class, ObsoleteCoroutinesApi::class)
    fun build(): DefaultGateway {
        val client = client ?: HttpClient(CIO) {
            install(WebSockets)
            install(JsonFeature)
        }
        val retry = reconnectRetry ?: LinearRetry(2.seconds, 20.seconds, 10)
        val sendRateLimiter = sendRateLimiter ?: BucketRateLimiter(120, 60.seconds)
        val identifyRateLimiter = identifyRateLimiter ?: BucketRateLimiter(1, 5.seconds)

        return DefaultGateway(DefaultGatewayData(url, client, retry, sendRateLimiter, identifyRateLimiter))
    }

}

package com.gitlab.kordlib.gateway

import com.gitlab.kordlib.common.entity.*
import kotlinx.serialization.*
import kotlinx.serialization.builtins.nullable
import kotlinx.serialization.builtins.serializer
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.json.JsonElement
import kotlinx.serialization.json.JsonObject
import mu.KotlinLogging

private val jsonLogger = KotlinLogging.logger { }

sealed class DispatchEvent : Event() {
    abstract val sequence: Int?
}

private object NullDecoder : DeserializationStrategy<Nothing?> {
    override val descriptor: SerialDescriptor
        get() = PrimitiveSerialDescriptor("null", PrimitiveKind.STRING)

    @OptIn(ExperimentalSerializationApi::class)
    override fun deserialize(decoder: Decoder): Nothing? {
        return decoder.decodeNull()
    }

    override fun patch(decoder: Decoder, old: Nothing?): Nothing? = throw NotImplementedError()
}

sealed class Event {
    companion object : DeserializationStrategy<Event?> {
        override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Event") {
            element("op", OpCode.descriptor)
            element("t", String.serializer().descriptor, isOptional = true)
            element("s", Int.serializer().descriptor, isOptional = true)
            element("d", JsonObject.serializer().descriptor, isOptional = true)
        }

        @OptIn(ExperimentalSerializationApi::class)
        override fun deserialize(decoder: Decoder): Event? {
            var op: OpCode? = null
            var data: Event? = null
            var sequence: Int? = null
            var eventName: String? = null
            with(decoder.beginStructure(descriptor)) {
                loop@ while (true) {
                    when (val index = decodeElementIndex(descriptor)) {//we assume the all fields to be present *before* the data field
                        CompositeDecoder.DECODE_DONE -> break@loop
                        0 -> {
                            op = OpCode.deserialize(decoder)
                            @Suppress("NON_EXHAUSTIVE_WHEN")
                            when (op) {
                                OpCode.HeartbeatACK -> data = HeartbeatACK
                                OpCode.Reconnect -> data = Reconnect
                            }
                        }
                        1 -> eventName = decodeNullableSerializableElement(descriptor, index, String.serializer().nullable)
                        2 -> sequence = decodeNullableSerializableElement(descriptor, index, Int.serializer().nullable)
                        3 -> data = when (op) {
                            OpCode.Dispatch -> getByDispatchEvent(index, this, eventName, sequence)
                            OpCode.Heartbeat -> decodeSerializableElement(descriptor, index, Heartbeat.serializer())
                            OpCode.HeartbeatACK -> {
                                this.decodeSerializableElement(descriptor, index, NullDecoder)
                                HeartbeatACK
                            }
                            OpCode.InvalidSession -> decodeSerializableElement(descriptor, index, InvalidSession)
                            OpCode.Hello -> decodeSerializableElement(descriptor, index, Hello.serializer())
                            //some events contain undocumented data fields, we'll only assume an unknown opcode with no data to be an error
                            else -> if(data == null) {
                                val element = decodeNullableSerializableElement(descriptor, index, JsonElement.serializer().nullable)
                                error("Unknown 'd' field for Op code ${op?.code}: $element")
                            } else {
                                decodeNullableSerializableElement(descriptor, index, JsonElement.serializer().nullable)
                                data
                            }
                        }
                    }
                }
                endStructure(descriptor)
                return data
            }
        }

        private fun getByDispatchEvent(index: Int, decoder: CompositeDecoder, name: String?, sequence: Int?) = when (name) {
            "PRESENCES_REPLACE" -> null //https://github.com/kordlib/kord/issues/42
            "RESUMED" -> Resumed(decoder.decodeSerializableElement(descriptor, index, ResumedData.serializer()), sequence)
            "READY" -> Ready(decoder.decodeSerializableElement(descriptor, index, ReadyData.serializer()), sequence)
            "CHANNEL_CREATE" -> ChannelCreate(decoder.decodeSerializableElement(descriptor, index, DiscordChannel.serializer()), sequence)
            "CHANNEL_UPDATE" -> ChannelUpdate(decoder.decodeSerializableElement(descriptor, index, DiscordChannel.serializer()), sequence)
            "CHANNEL_DELETE" -> ChannelDelete(decoder.decodeSerializableElement(descriptor, index, DiscordChannel.serializer()), sequence)
            "CHANNEL_PINS_UPDATE" -> ChannelPinsUpdate(decoder.decodeSerializableElement(descriptor, index, DiscordPinsUpdateData.serializer()), sequence)
            "TYPING_START" -> TypingStart(decoder.decodeSerializableElement(descriptor, index, DiscordTyping.serializer()), sequence)
            "GUILD_CREATE" -> GuildCreate(decoder.decodeSerializableElement(descriptor, index, DiscordGuild.serializer()), sequence)
            "GUILD_UPDATE" -> GuildUpdate(decoder.decodeSerializableElement(descriptor, index, DiscordGuild.serializer()), sequence)
            "GUILD_DELETE" -> GuildDelete(decoder.decodeSerializableElement(descriptor, index, DiscordUnavailableGuild.serializer()), sequence)
            "GUILD_BAN_ADD" -> GuildBanAdd(decoder.decodeSerializableElement(descriptor, index, DiscordGuildBan.serializer()), sequence)
            "GUILD_BAN_REMOVE" -> GuildBanRemove(decoder.decodeSerializableElement(descriptor, index, DiscordGuildBan.serializer()), sequence)
            "GUILD_EMOJIS_UPDATE" -> GuildEmojisUpdate(decoder.decodeSerializableElement(descriptor, index, DiscordUpdatedEmojis.serializer()), sequence)
            "GUILD_INTEGRATIONS_UPDATE" -> GuildIntegrationsUpdate(decoder.decodeSerializableElement(descriptor, index, DiscordGuildIntegrations.serializer()), sequence)
            "GUILD_MEMBER_ADD" -> GuildMemberAdd(decoder.decodeSerializableElement(descriptor, index, DiscordAddedGuildMember.serializer()), sequence)
            "GUILD_MEMBER_REMOVE" -> GuildMemberRemove(decoder.decodeSerializableElement(descriptor, index, DiscordRemovedGuildMember.serializer()), sequence)
            "GUILD_MEMBER_UPDATE" -> GuildMemberUpdate(decoder.decodeSerializableElement(descriptor, index, DiscordUpdatedGuildMember.serializer()), sequence)
            "GUILD_ROLE_CREATE" -> GuildRoleCreate(decoder.decodeSerializableElement(descriptor, index, DiscordGuildRole.serializer()), sequence)
            "GUILD_ROLE_UPDATE" -> GuildRoleUpdate(decoder.decodeSerializableElement(descriptor, index, DiscordGuildRole.serializer()), sequence)
            "GUILD_ROLE_DELETE" -> GuildRoleDelete(decoder.decodeSerializableElement(descriptor, index, DiscordDeletedGuildRole.serializer()), sequence)
            "GUILD_MEMBERS_CHUNK" -> GuildMembersChunk(decoder.decodeSerializableElement(descriptor, index, GuildMembersChunkData.serializer()), sequence)

            "INVITE_CREATE" -> InviteCreate(decoder.decodeSerializableElement(descriptor, index, DiscordCreatedInvite.serializer()), sequence)
            "INVITE_DELETE" -> InviteDelete(decoder.decodeSerializableElement(descriptor, index, DiscordDeletedInvite.serializer()), sequence)

            "MESSAGE_CREATE" -> MessageCreate(decoder.decodeSerializableElement(descriptor, index, DiscordMessage.serializer()), sequence)
            "MESSAGE_UPDATE" -> MessageUpdate(decoder.decodeSerializableElement(descriptor, index, DiscordPartialMessage.serializer()), sequence)
            "MESSAGE_DELETE" -> MessageDelete(decoder.decodeSerializableElement(descriptor, index, DeletedMessage.serializer()), sequence)
            "MESSAGE_DELETE_BULK" -> MessageDeleteBulk(decoder.decodeSerializableElement(descriptor, index, BulkDeleteData.serializer()), sequence)
            "MESSAGE_REACTION_ADD" -> MessageReactionAdd(decoder.decodeSerializableElement(descriptor, index, MessageReaction.serializer()), sequence)
            "MESSAGE_REACTION_REMOVE" -> MessageReactionRemove(decoder.decodeSerializableElement(descriptor, index, MessageReaction.serializer()), sequence)
            "MESSAGE_REACTION_REMOVE_EMOJI" -> MessageReactionRemoveEmoji(decoder.decodeSerializableElement(descriptor, index, DiscordRemovedEmoji.serializer()), sequence)

            "MESSAGE_REACTION_REMOVE_ALL" -> MessageReactionRemoveAll(decoder.decodeSerializableElement(descriptor, index, AllRemovedMessageReactions.serializer()), sequence)
            "PRESENCE_UPDATE" -> PresenceUpdate(decoder.decodeSerializableElement(descriptor, index, DiscordPresenceUpdateData.serializer()), sequence)
            "USER_UPDATE" -> UserUpdate(decoder.decodeSerializableElement(descriptor, index, DiscordUser.serializer()), sequence)
            "VOICE_STATE_UPDATE" -> VoiceStateUpdate(decoder.decodeSerializableElement(descriptor, index, DiscordVoiceState.serializer()), sequence)
            "VOICE_SERVER_UPDATE" -> VoiceServerUpdate(decoder.decodeSerializableElement(descriptor, index, DiscordVoiceServerUpdateData.serializer()), sequence)
            "WEBHOOKS_UPDATE" -> WebhooksUpdate(decoder.decodeSerializableElement(descriptor, index, DiscordWebhooksUpdateData.serializer()), sequence)
            else -> {
                jsonLogger.warn { "unknown gateway event name $name" }
                // consume json elements that are unknown to us
                decoder.decodeSerializableElement(descriptor, index, JsonElement.serializer().nullable)
                null
            }
        }

        override fun patch(decoder: Decoder, old: Event?): Event? = error("")
    }

}

sealed class Close : Event() {

    /**
     * The Gateway was detached, all resources tied to the gateway should be freed.
     */
    object Detach: Close()

    /**
     * The user closed the Gateway connection.
     */
    object UserClose: Close()

    /**
     * The connection was closed because of a timeout, probably due to a loss of internet connection.
     */
    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: GatewayCloseCode, val recoverable: Boolean): Close()

    /**
     * The gateway closed and will attempt to resume the session.
     */
    object Reconnecting : Close()

    /**
     * The gateway closed and will attempt to start a new session.
     */
    object SessionReset : Close()

    /**
     * Discord is no longer responding to the gateway commands, the connection will be closed and an attempt to resume the session will be made.
     * Any [commands][Command] send recently might not complete, and won't be automatically requeued.
     */
    object ZombieConnection : 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 [Gateway.start], otherwise all resources linked to the Gateway should free and the Gateway [detached][Gateway.detach].
     */
    object RetryLimitReached: Close()
}

object HeartbeatACK : Event()
object Reconnect : Event()

@Serializable
data class Hello(
        @SerialName("heartbeat_interval")
        val heartbeatInterval: Long,
        @SerialName("_trace")
        val traces: List<String>
) : Event()

data class Ready(val data: ReadyData, override val sequence: Int?) : DispatchEvent()

@Serializable
data class ReadyData(
        @SerialName("v")
        val version: Int,
        val user: DiscordUser,
        @SerialName("private_channels")
        val privateChannels: List<DiscordChannel>, //TODO("Add DM Channel.")
        val guilds: List<DiscordUnavailableGuild>,
        @SerialName("session_id")
        val sessionId: String,
        @SerialName("_trace")
        val traces: List<String>,
        val shard: DiscordShard?)

@Serializable
data class Heartbeat(val data: Long) : Event() {
    @Serializer(Heartbeat::class)
    companion object : DeserializationStrategy<Heartbeat> {
        override val descriptor: SerialDescriptor
            get() = PrimitiveSerialDescriptor("HeartbeatEvent", PrimitiveKind.LONG)

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

@Serializable
data class Resumed(val data: ResumedData, override val sequence: Int?) : DispatchEvent()

@Serializable
data class ResumedData(
        @SerialName("_trace")
        val traces: List<String>
) {
    @Serializer(Heartbeat::class)
    companion object : DeserializationStrategy<Heartbeat> {
        override val descriptor: SerialDescriptor
            get() = PrimitiveSerialDescriptor("HeartbeatEvent", PrimitiveKind.LONG)

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

@Serializable
data class InvalidSession(val resumable: Boolean) : Event() {
    @Serializer(InvalidSession::class)
    companion object : DeserializationStrategy<InvalidSession> {
        override val descriptor: SerialDescriptor
            get() = PrimitiveSerialDescriptor("InvalidSession", PrimitiveKind.BOOLEAN)

        override fun deserialize(decoder: Decoder) = InvalidSession(decoder.decodeBoolean())
    }
}

data class ChannelCreate(val channel: DiscordChannel, override val sequence: Int?) : DispatchEvent()
data class ChannelUpdate(val channel: DiscordChannel, override val sequence: Int?) : DispatchEvent()
data class ChannelDelete(val channel: DiscordChannel, override val sequence: Int?) : DispatchEvent()
data class ChannelPinsUpdate(val pins: DiscordPinsUpdateData, override val sequence: Int?) : DispatchEvent()

data class TypingStart(val data: DiscordTyping, override val sequence: Int?) : DispatchEvent()
data class GuildCreate(val guild: DiscordGuild, override val sequence: Int?) : DispatchEvent()
data class GuildUpdate(val guild: DiscordGuild, override val sequence: Int?) : DispatchEvent()
data class GuildDelete(val guild: DiscordUnavailableGuild, override val sequence: Int?) : DispatchEvent()
data class GuildBanAdd(val ban: DiscordGuildBan, override val sequence: Int?) : DispatchEvent()
data class GuildBanRemove(val ban: DiscordGuildBan, override val sequence: Int?) : DispatchEvent()
data class GuildEmojisUpdate(val emoji: DiscordUpdatedEmojis, override val sequence: Int?) : DispatchEvent()
data class GuildIntegrationsUpdate(val integrations: DiscordGuildIntegrations, override val sequence: Int?) : DispatchEvent()
data class GuildMemberAdd(val member: DiscordAddedGuildMember, override val sequence: Int?) : DispatchEvent()
data class GuildMemberRemove(val member: DiscordRemovedGuildMember, override val sequence: Int?) : DispatchEvent()
data class GuildMemberUpdate(val member: DiscordUpdatedGuildMember, override val sequence: Int?) : DispatchEvent()
data class GuildRoleCreate(val role: DiscordGuildRole, override val sequence: Int?) : DispatchEvent()
data class GuildRoleUpdate(val role: DiscordGuildRole, override val sequence: Int?) : DispatchEvent()
data class GuildRoleDelete(val role: DiscordDeletedGuildRole, override val sequence: Int?) : DispatchEvent()
data class GuildMembersChunk(val data: GuildMembersChunkData, override val sequence: Int?) : DispatchEvent()

/**
 * Sent when a new invite to a channel is created.
 */
data class InviteCreate(val invite: DiscordCreatedInvite, override val sequence: Int?) : DispatchEvent()

/**
 * Sent when an invite is deleted.
 */
data class InviteDelete(val invite: DiscordDeletedInvite, override val sequence: Int?) : DispatchEvent()

@Serializable
data class DiscordDeletedInvite(
        /**
         * The channel of the invite.
         */
        @SerialName("channel_id")
        val channelId: String,
        /**
         * The guild of the invite.
         */
        @SerialName("guild_id")
        val guildId: String,
        /**
         * The unique invite code.
         */
        val code: String
)

@Serializable
data class DiscordCreatedInvite(
        /**
         * The channel the invite is for.
         */
        @SerialName("channel_id")
        val channelId: String,
        /**
         * The unique invite code.
         */
        val code: String,
        /**
         * The time at which the invite was created.
         */
        @SerialName("created_at")
        val createdAt: String,
        /**
         * The guild of the invite.
         */
        @SerialName("guild_id")
        val guildId: String,
        /**
         * The user that created the invite.
         */
        val inviter: DiscordInviteUser,
        /**
         * How long the invite is valid for (in seconds).
         */
        @SerialName("max_age")
        val maxAge: Int,
        /**
         * The maximum number of times the invite can be used.
         */
        @SerialName("max_uses")
        val maxUses: Int,
        /**
         * Whether or not the invite is temporary (invited users will be kicked on disconnect unless they're assigned a role).
         */
        val temporary: Boolean,
        /**
         * How many times the invite has been used (always will be 0).
         */
        val uses: Int,

        /**
         * The target user for this invite.
         */
        @SerialName("target_user")
        val targetUser: DiscordInviteUser? = null,

        /**
         * The type of user target for this invite.
         */
        @SerialName("target_user_type")
        val targetUserType: TargetUserType? = null
)

@Serializable
data class DiscordInviteUser(
        val avatar: String? = null,
        val discriminator: String,
        val id: String,
        val username: String
)

data class MessageCreate(val message: DiscordMessage, override val sequence: Int?) : DispatchEvent()
data class MessageUpdate(val message: DiscordPartialMessage, override val sequence: Int?) : DispatchEvent()
data class MessageDelete(val message: DeletedMessage, override val sequence: Int?) : DispatchEvent()
data class MessageDeleteBulk(val messageBulk: BulkDeleteData, override val sequence: Int?) : DispatchEvent()
data class MessageReactionAdd(val reaction: MessageReaction, override val sequence: Int?) : DispatchEvent()
data class MessageReactionRemove(val reaction: MessageReaction, override val sequence: Int?) : DispatchEvent()
data class MessageReactionRemoveAll(val reactions: AllRemovedMessageReactions, override val sequence: Int?) : DispatchEvent()
data class MessageReactionRemoveEmoji(val reaction: DiscordRemovedEmoji, override val sequence: Int?) : DispatchEvent()

@Serializable
data class DiscordRemovedEmoji(
        /**
         * The id of the channel.
         */
        @SerialName("channel_id")
        val channelId: String,

        /**
         * The id of the guild.
         */
        @SerialName("guild_id")
        val guildId: String,

        /**
         * The id of the message.
         */
        @SerialName("message_id")
        val messageId: String,

        /**
         * The emoji that was removed.
         */
        val emoji: DiscordRemovedReactionEmoji
)

@Serializable
data class DiscordRemovedReactionEmoji(
        /**
         * The id of the emoji.
         */
        val id: String?,
        /**
         * The name of the emoji.
         */
        val name: String
)

data class PresenceUpdate(val presence: DiscordPresenceUpdateData, override val sequence: Int?) : DispatchEvent()
data class UserUpdate(val user: DiscordUser, override val sequence: Int?) : DispatchEvent()
data class VoiceStateUpdate(val voiceState: DiscordVoiceState, override val sequence: Int?) : DispatchEvent()
data class VoiceServerUpdate(val voiceServerUpdateData: DiscordVoiceServerUpdateData, override val sequence: Int?) : DispatchEvent()
data class WebhooksUpdate(val webhooksUpdateData: DiscordWebhooksUpdateData, override val sequence: Int?) : DispatchEvent()

package com.gitlab.kordlib.gateway

import com.gitlab.kordlib.common.entity.DiscordShard
import com.gitlab.kordlib.common.entity.Status
import com.gitlab.kordlib.gateway.builder.PresenceBuilder
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.time.Duration

/**
 * An implementation of the Discord [Gateway](https://discord.com/developers/docs/topics/gateway) and its lifecycle.
 *
 * Allows consumers to receive [events](https://discord.com/developers/docs/topics/gateway#commands-and-events-gateway-events)
 * through [events] and send [commands](https://discord.com/developers/docs/topics/gateway#commands-and-events-gateway-commands)
 * through [send].
 */
interface Gateway {
    /**
     * The incoming [events](https://discord.com/developers/docs/topics/gateway#commands-and-events-gateway-events)
     * of the Gateway. Users should expect these [Flows](Flow) to be hot and remain open for the entire lifecycle of the
     * Gateway.
     */
    val events: Flow<Event>

    /**
     * The duration between the last [Heartbeat] and [HeartbeatACK]. If no [Heartbeat] has been received yet,
     * [Duration.INFINITE] will be returned.
     */
    val ping: Duration

    /**
     * Starts a reconnection 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.
     */
    suspend fun start(configuration: GatewayConfiguration)

    /**
     * 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 connection isn't open.
     */
    suspend fun send(command: Command)

    /**
     * Close gateway and releases resources.
     *
     * **For some implementations this will render the Gateway unopenable,
     * as such, all implementations should be handled as if they are irreversibly closed.**
     */
    suspend fun detach()

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

    companion object {
        private object None : Gateway {

            override val events: Flow<Event>
                get() = emptyFlow()

            override val ping: Duration
                get() = Duration.ZERO

            override suspend fun send(command: Command) {}

            override suspend fun start(configuration: GatewayConfiguration) {}

            override suspend fun stop() {}

            override suspend fun detach() {}
        }

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

    }
}

@OptIn(ExperimentalContracts::class)
suspend inline fun Gateway.editPresence(builder: PresenceBuilder.() -> Unit) {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    val status = PresenceBuilder().apply(builder).toUpdateStatus()
    send(status)
}

/**
 * Starts a reconnecting gateway connection with the given parameters.
 * This function will suspend until the lifecycle of the gateway has ended.
 *
 * ```kotlin
 * gateway.start("your_token") {
 *     shard = DiscordShard(0,1)
 *
 *     presence {
 *         afk = false
 *         status = Status.Online
 *         watching("you :eyes:")
 *     }
 *
 * }
 *
 * //gateway has disconnected
 * ```
 *
 * @param token The Discord token of the bot.
 * @param config additional configuration for the gateway.
 */
@OptIn(ExperimentalContracts::class)
suspend inline fun Gateway.start(token: String, config: GatewayConfigurationBuilder.() -> Unit = {}) {
    contract {
        callsInPlace(config, InvocationKind.EXACTLY_ONCE)
    }
    val builder = GatewayConfigurationBuilder(token)
    builder.apply(config)
    start(builder.build())
}

/**
 * Enum representation of Discord's [Gateway close event codes](https://discord.com/developers/docs/topics/opcodes-and-status-codes#gateway-gateway-close-event-codes).
 */
enum class GatewayCloseCode(val code: Int) {
    Unknown(4000),
    UnknownOpCode(4001),
    DecodeError(4002),
    NotAuthenticated(4003),
    AuthenticationFailed(4004),
    AlreadyAuthenticated(4005),
    InvalidSeq(4007),
    RateLimited(4008),
    SessionTimeout(4009),
    InvalidShard(4010),
    ShardingRequired(4011),
    InvalidApiVersion(4012),
    InvalidIntents(4013),
    DisallowedIntents(4014)
}

package com.gitlab.kordlib.gateway

import com.gitlab.kordlib.common.entity.DiscordShard
import com.gitlab.kordlib.gateway.builder.PresenceBuilder
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

data class GatewayConfiguration(
        val token: String,
        val name: String,
        val shard: DiscordShard,
        val presence: Presence?,
        val threshold: Int,
        val intents: Intents?
)

data class GatewayConfigurationBuilder(
        /**
         * The token of the bot.
         */
        val token: String,
        /**
         * The name of the library.
         */
        var name: String = "Kord",
        /**
         * The shard the gateway will connect to.
         */
        var shard: DiscordShard = DiscordShard(0, 1),
        /**
         * The presence the bot should show on login.
         */
        var presence: Presence? = null,
        /**
         * A value between 50 and 250, representing the maximum amount of members in a guild
         * before the gateway will stop sending info on offline members.
         */
        var threshold: Int = 250,

        var intents: Intents? = null
) {

    /**
     * Calls the [builder] on a new [PresenceBuilder] and assigns the result to [presence].
     */
    @OptIn(ExperimentalContracts::class)
    inline fun presence(builder: PresenceBuilder.() -> Unit) {
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }
        presence = PresenceBuilder().apply(builder).toPresence()
    }

    /**
     * Returns an immutable version of this builder.
     */
    fun build(): GatewayConfiguration = GatewayConfiguration(token, name, shard, presence, threshold, intents)

    companion object
}

package com.gitlab.kordlib.gateway

import kotlinx.serialization.*
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
import kotlin.RequiresOptIn.*
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

/**
 * Some intents are defined as "Privileged" due to the sensitive nature of the data and cannot be used by Kord without enabling them.
 *
 * See [the official documentation](https://discord.com/developers/docs/topics/gateway#privileged-intents) for more info on
 * how to enable these.
 */
@RequiresOptIn("""
    Some intents are defined as "Privileged" due to the sensitive nature of the data and cannot be used by Kord without enabling them.

    See https://discord.com/developers/docs/topics/gateway#privileged-intents for more info on how to enable these.
""", Level.ERROR)
annotation class PrivilegedIntent

/**
 * Values that enable a group of events as [defined by Discord](https://github.com/discord/discord-api-docs/blob/feature/gateway-intents/docs/topics/Gateway.md#gateway-intents).
 */
enum class Intent(val code: Int) {
    /**
     * Enables the following events:
     * - [GuildCreate]
     * - [GuildDelete]
     * - [GuildRoleCreate]
     * - [GuildRoleUpdate]
     * - [GuildRoleDelete]
     * - [ChannelCreate]
     * - [ChannelUpdate]
     * - [ChannelDelete]
     * - [ChannelPinsUpdate]
     */
    Guilds(1 shl 0),

    /**
     * Enables the following events:
     * - [GuildMemberAdd]
     * - [GuildMemberUpdate]
     * - [GuildMemberRemove]
     */
    @PrivilegedIntent
    GuildMembers(1 shl 1),

    /**
     * Enables the following events:
     * - [GuildBanAdd]
     * - [GuildBanRemove]
     */
    GuildBans(1 shl 2),

    /**
     * Enables the following events:
     * - [GuildEmojisUpdate]
     */
    GuildEmojis(1 shl 3),

    /**
     * Enables the following events:
     * - [GuildIntegrationsUpdate]
     */
    GuildIntegrations(1 shl 4),

    /**
     * Enables the following events:
     * - [WebhooksUpdate]
     */
    GuildWebhooks(1 shl 5),

    /**
     * Enables the following events:
     * - INVITE_CREATE
     * - INVITE_DELETE
     */
    GuildInvites(1 shl 6),

    /**
     * Enables the following events:
     * - [VoiceStateUpdate]
     */
    GuildVoiceStates(1 shl 7),

    /**
     * Enables the following events:
     * - [PresenceUpdate]
     */
    @PrivilegedIntent
    GuildPresences(1 shl 8),

    /**
     * Enables the following events:
     * - [MessageCreate]
     * - [MessageUpdate]
     * - [MessageDelete]
     * - [MessageDeleteBulk]
     */
    GuildMessages(1 shl 9),

    /**
     * Enables the following events:
     * - [MessageReactionAdd]
     * - [MessageReactionRemove]
     * - [MessageReactionRemoveAll]
     * - MESSAGE_REACTION_REMOVE_EMOJI
     */
    GuildMessageReactions(1 shl 10),

    /**
     * Enables the following events:
     * - [TypingStart]
     */
    GuildMessageTyping(1 shl 11),

    /**
     * Enables the following events:
     * - [ChannelCreate]
     * - [ChannelDelete]
     * - [MessageUpdate]
     * - [MessageDelete]
     */
    DirectMessages(1 shl 12),

    /**
     * Enables the following events:
     * - [MessageReactionAdd]
     * - [MessageReactionRemove]
     * - [MessageReactionRemoveAll]
     * - MESSAGE_REACTION_REMOVE_EMOJI
     */
    DirectMessagesReactions(1 shl 13),

    /**
     * Enables the following events:
     * - [TypingStart]
     */
    DirectMessageTyping(1 shl 14)
}

/**
 * A set of [intents][Intent] to be used while [identifying][Identify] a [Gateway] connection to communicate the events the client wishes to receive.
 */
@Serializable(with = IntentsSerializer::class)
data class Intents internal constructor(val code: Int) {

    val intents = Intent.values().filter { code and it.code != 0 }.toSet()

    operator fun contains(intent: Intent) = intent in intents

    /**
     * Returns an [Intents] that added the [intent] to this [code].
     */
    operator fun plus(intent: Intent): Intents = when {
        code and intent.code == intent.code -> this
        else -> Intents(this.code or intent.code)
    }

    /**
     * Returns an [Intents] that removed the [intent] from this [code].
     */
    operator fun minus(intent: Intent): Intents = when {
        code and intent.code == intent.code -> Intents(code xor intent.code)
        else -> this
    }

    /**
     * copy this [Intents] and apply the [block] to it.
     */
    @OptIn(ExperimentalContracts::class)
    inline fun copy(block: IntentsBuilder.() -> Unit): Intents {
        contract {
            callsInPlace(block, InvocationKind.EXACTLY_ONCE)
        }
        val builder = IntentsBuilder(code)
        builder.apply(block)
        return builder.flags()
    }

    companion object {

        @PrivilegedIntent
        val all: Intents
            get() = invoke {
                Intent.values().forEach { +it }
            }

        @OptIn(PrivilegedIntent::class)
        val nonPrivileged: Intents
            get() = invoke {
                Intent.values().forEach { +it }

                -Intent.GuildPresences
                -Intent.GuildMembers
            }

        val none: Intents = invoke()

        inline operator fun invoke(builder: IntentsBuilder.() -> Unit = {}): Intents {
            return IntentsBuilder().apply(builder).flags()
        }
    }

    class IntentsBuilder(internal var code: Int = 0) {
        operator fun Intents.unaryPlus() {
            this@IntentsBuilder.code = this@IntentsBuilder.code or code
        }

        operator fun Intent.unaryPlus() {
            this@IntentsBuilder.code = this@IntentsBuilder.code or code
        }

        operator fun Intent.unaryMinus() {
            if (this@IntentsBuilder.code and code == code) {
                this@IntentsBuilder.code = this@IntentsBuilder.code xor code
            }
        }

        fun flags() = Intents(code)
    }

}

@Serializer(forClass = Intents::class)
object IntentsSerializer : KSerializer<Intents> {
    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("intents", PrimitiveKind.INT)

    override fun deserialize(decoder: Decoder): Intents {
        val flags = decoder.decodeInt()
        return Intents(flags)
    }

    override fun serialize(encoder: Encoder, obj: Intents) {
        encoder.encodeInt(obj.code)
    }
}

package com.gitlab.kordlib.gateway

import kotlinx.serialization.*
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
import kotlinx.serialization.internal.IntDescriptor

@Serializable(with = OpCode.OpCodeSerializer::class)
enum class OpCode(val code: Int) {
    /** The default code for unknown values. */
    Unknown(Int.MIN_VALUE),
    Dispatch(0),
    Heartbeat(1),
    Identify(2),
    StatusUpdate(3),
    VoiceStateUpdate(4),
    Resume(6),
    Reconnect(7),
    RequestGuildMembers(8),
    InvalidSession(9),
    Hello(10),
    HeartbeatACK(11);

    @Serializer(forClass = OpCode::class)
    companion object OpCodeSerializer : KSerializer<OpCode> {
        override val descriptor: SerialDescriptor
            get() = PrimitiveSerialDescriptor("op", PrimitiveKind.INT)

        override fun deserialize(decoder: Decoder): OpCode {
            val code = decoder.decodeInt()
            return values().firstOrNull { it.code == code } ?: Unknown
        }

        override fun serialize(encoder: Encoder, obj: OpCode) {
            encoder.encodeInt(obj.code)
        }
    }

}

package com.gitlab.kordlib.gateway

import kotlinx.atomicfu.AtomicRef
import kotlinx.atomicfu.atomic
import kotlinx.atomicfu.update

internal class Sequence {
    private val counter: AtomicRef<Int?> = atomic(null)

    var value
        get() = counter.value
        set(value) = counter.update { value }
}

package com.gitlab.kordlib.gateway

import io.ktor.util.error
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.channels.ticker
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import mu.KotlinLogging
import kotlin.coroutines.CoroutineContext

private val logger = KotlinLogging.logger {  }

/**
 * A reusable fixed rate ticker.
 *
 * @param dispatcher The dispatchers the events will be fired on.
 */
@ObsoleteCoroutinesApi
class Ticker(private val dispatcher: CoroutineDispatcher = Dispatchers.Default) : CoroutineScope {

    override val coroutineContext: CoroutineContext
        get() = Job() + dispatcher

    private val mutex = Mutex()

    private var ticker: ReceiveChannel<Unit>? = null

    suspend fun tickAt(intervalMillis: Long, block: suspend () -> Unit) {
        stop()
        mutex.withLock {
            ticker = ticker(intervalMillis)
            launch {
                ticker?.consumeEach {
                    try {
                        block()
                    } catch (exception: Exception) {
                        logger.error(exception)
                    }
                }
            }
        }
    }

    suspend fun stop() {
        mutex.withLock {
            ticker?.cancel()
        }
    }

}

package com.gitlab.kordlib.rest

import io.ktor.client.HttpClient
import io.ktor.client.request.request
import io.ktor.client.statement.HttpResponse
import io.ktor.http.HttpMethod
import io.ktor.util.toByteArray
import kotlinx.coroutines.Dispatchers
import java.util.*

class Image private constructor(val data: ByteArray, val format: Format) {

    val dataUri: String
        get() {
            val hash = Base64.getEncoder().encodeToString(data)
            return "data:image/${format.extension};base64,$hash"
        }

    companion object {
        fun raw(data: ByteArray, format: Format): Image {
            return Image(data, format)
        }

        suspend fun fromUrl(client: HttpClient, url: String): Image = with(Dispatchers.IO) {
            val call = client.request<HttpResponse>(url) { method = HttpMethod.Get }
            val contentType = call.headers["Content-Type"]
                    ?: error("expected 'Content-Type' header in image request")

            @Suppress("EXPERIMENTAL_API_USAGE")
            val bytes = call.content.toByteArray()

            Image(bytes, Format.fromContentType(contentType))
        }
    }

    sealed class Format(val extension: String) {
        object JPEG : Format("jpeg")
        object PNG : Format("png")
        object WEBP : Format("webp")
        object GIF : Format("gif")

        companion object {
            fun fromContentType(type: String) = when (type) {
                "image/jpeg" -> JPEG
                "image/png" -> PNG
                "image/webp" -> WEBP
                "image/gif" -> GIF
                else -> error(type)
            }
        }
    }
}

package com.gitlab.kordlib.common.annotation

/**
 * Dsl marker for Kord dsls.
 */
@DslMarker
@Target(AnnotationTarget.CLASS)
annotation class KordDsl

/**
 * Marks a Kord-related API as a feature preview.
 *
 * A Kord preview has **no** backward compatibility guarantees, including both binary and source compatibility.
 * Its API and semantics can and will be changed in next releases.
 *
 * Features marked with this annotation will have its api evaluated and changed over time.
 */
@MustBeDocumented
@Retention(value = AnnotationRetention.BINARY)
@Experimental(level = Experimental.Level.WARNING)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.TYPEALIAS, AnnotationTarget.PROPERTY)
annotation class KordPreview

/**
 * Marks a Kord-related API as experimental.
 *
 * Kord experimental has **no** backward compatibility guarantees, including both binary and source compatibility.
 * Its API and semantics can and will be changed in next releases.
 *
 * Features marked with this annotation will have its use evaluated and changed over time
 * and might not make it into the stable api.
 */
@MustBeDocumented
@Retention(value = AnnotationRetention.BINARY)
@Experimental(level = Experimental.Level.WARNING)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.TYPEALIAS, AnnotationTarget.PROPERTY)
annotation class KordExperimental

/**
 * Marks a Kord-related API as potentially unsafe.
 *
 * Kord marks targets as unsafe if it exposes functionality in a way that is more error prone than alternatives
 * and can lead to *inconsistent state* and *fail silent* or *fail slow* code.
 *
 * The trade off is usually increased performance by reducing cache hits and requests to the discord api.
 *
 * Functionality that is annotated with KordUnsafe should suggest a safer alternative approach in its documentation.
 */
@MustBeDocumented
@Retention(value = AnnotationRetention.BINARY)
@Experimental(level = Experimental.Level.WARNING)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.TYPEALIAS, AnnotationTarget.PROPERTY)
annotation class KordUnsafe

package com.gitlab.kordlib.common.entity

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class DiscordPinsUpdateData(
        @SerialName("guild_id")
        val guildId: String,
        @SerialName("channel_id")
        val channelId: String,
        @SerialName("last_pin_timestamp")
        val lastPinTimestamp: String? = null
)

@Serializable
data class DiscordTyping(
        @SerialName("channel_id")
        val channelId: String,
        @SerialName("guild_id")
        val guildId: String? = null,
        @SerialName("user_id")
        val userId: String,
        val timestamp: Long
)

package com.gitlab.kordlib.common.entity

import kotlinx.serialization.*
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
data class DiscordActivity(
        val name: String,
        val type: ActivityType,
        val url: String? = null,
        val timestamps: DiscordActivityTimeStamps? = null,
        @SerialName("application_id")
        val applicationId: String? = null,
        val details: String? = null,
        val emoji: DiscordPartialEmoji? = null,
        val state: String? = null,
        val party: DiscordActivityParty? = null,
        val assets: DiscordActivityAssets? = null,
        val secrets: DiscordActivitySecrets? = null,
        val instance: Boolean? = null,
        val flags: Int? = null
)

@Serializable
data class DiscordActivityTimeStamps(
        val start: Long? = null,
        val end: Long? = null
)

@Serializable
data class DiscordActivityParty(
        val id: String? = null,
        val size: List<Int>? = null
)

@Serializable
data class DiscordActivityAssets(
        @SerialName("large_image")
        val largeImage: String? = null,
        @SerialName("large_text")
        val largeText: String? = null,
        @SerialName("small_image")
        val smallImage: String? = null,
        @SerialName("small_text")
        val smallText: String? = null
)

@Serializable
data class DiscordActivitySecrets(
        val join: String? = null,
        val spectate: String? = null,
        val match: String? = null
)

@Serializable(with = ActivityType.ActivityTypeSerializer::class)
enum class ActivityType(val code: Int) {
    /** The default code for unknown values. */
    Unknown(Int.MIN_VALUE),
    Game(0),
    Streaming(1),
    Listening(2),
    Watching(3),
    Custom(4);

    @Serializer(forClass = ActivityType::class)
    companion object ActivityTypeSerializer : KSerializer<ActivityType> {
        override val descriptor: SerialDescriptor
            get() = PrimitiveSerialDescriptor("op", PrimitiveKind.INT)

        override fun deserialize(decoder: Decoder): ActivityType {
            val code = decoder.decodeInt()
            return values().firstOrNull { it.code == code } ?: Unknown
        }

        override fun serialize(encoder: Encoder, value: ActivityType) {
            encoder.encodeInt(value.code)
        }
    }

}

package com.gitlab.kordlib.common.entity

import kotlinx.serialization.*
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
data class DiscordChannel(
        val id: String,
        val type: ChannelType,
        @SerialName("guild_id")
        val guildId: String? = null,
        val position: Int? = null,
        @SerialName("permission_overwrites")
        val permissionOverwrites: List<Overwrite>? = null,
        val name: String? = null,
        val topic: String? = null,
        val nsfw: Boolean? = null,
        @SerialName("last_message_id")
        val lastMessageId: String? = null,
        val bitrate: Int? = null,
        @SerialName("user_limit")
        val userLimit: Int? = null,
        @SerialName("rate_limit_per_user")
        val rateLimitPerUser: Int? = null,
        val recipients: List<DiscordUser>? = null,
        val icon: String? = null,
        @SerialName("owner_id")
        val ownerId: String? = null,
        @SerialName("application_id")
        val applicationId: String? = null,
        @SerialName("parent_id")
        val parentId: String? = null,
        @SerialName("last_pin_timestamp")
        val lastPinTimestamp: String? = null
)

@Serializable
data class Overwrite(
        val id: String,
        val type: String,
        val allow: Int,
        val deny: Int
)

@Serializable(with = ChannelType.ChannelTypeSerializer::class)
enum class ChannelType(val code: Int) {
    /** The default code for unknown values. */
    Unknown(Int.MIN_VALUE),
    GuildText(0),
    DM(1),
    GuildVoice(2),
    GroupDm(3),
    GuildCategory(4),
    GuildNews(5),
    GuildStore(6);
    @Serializer(forClass = ChannelType::class)
    companion object ChannelTypeSerializer : KSerializer<ChannelType> {
        override val descriptor: SerialDescriptor
            get() = PrimitiveSerialDescriptor("type", PrimitiveKind.INT)

        override fun deserialize(decoder: Decoder): ChannelType {
            val code = decoder.decodeInt()
            return values().firstOrNull { it.code == code } ?: Unknown
        }

        override fun serialize(encoder: Encoder, value: ChannelType) {
            encoder.encodeInt(value.code)
        }
    }

}

package com.gitlab.kordlib.common.entity

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class DiscordEmoji(
        val id: String? = null,
        val name: String? = null,
        val roles: List<String>? = null,
        val user: DiscordUser? = null,
        @SerialName("require_colons")
        val requireColons: Boolean? = null,
        val managed: Boolean? = null,
        val animated: Boolean? = null,
        val available: Boolean? = null
)

@Serializable
data class DiscordUpdatedEmojis(
        @SerialName("guild_id")
        val guildId: String,
        val emojis: List<DiscordEmoji>
)

@Serializable
data class DiscordPartialEmoji(
        val id: String? = null,
        val name: String? = null,
        val animated: Boolean? = null
)

package com.gitlab.kordlib.common.entity

import kotlinx.serialization.*
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
import kotlinx.serialization.internal.IntDescriptor
import kotlinx.serialization.internal.StringDescriptor

@Serializable
data class DiscordUnavailableGuild(
        val id: String,
        val unavailable: Boolean? = null
)

@Serializable
data class DiscordGuild(
        val id: String,
        val name: String,
        val icon: String? = null,
        val splash: String? = null,
        @SerialName("discovery_splash")
        val discoverySplash: String? = null,
        val owner: Boolean? = null,
        @SerialName("owner_id")
        val ownerId: String,
        val permissions: Permissions? = null,
        val region: String,
        @SerialName("afk_channel_id")
        val afkChannelId: String? = null,
        @SerialName("afk_timeout")
        val afkTimeout: Int,
        @SerialName("embed_enabled")
        val embedEnabled: Boolean? = null,
        @SerialName("embed_channel_id")
        val embedChannelId: String? = null,
        @SerialName("verification_level")
        val verificationLevel: VerificationLevel,
        @SerialName("default_message_notifications")
        val defaultMessageNotifications: DefaultMessageNotificationLevel,
        @SerialName("explicit_content_filter")
        val explicitContentFilter: ExplicitContentFilter,
        val roles: List<DiscordRole>,
        val emojis: List<DiscordEmoji>,
        val features: List<GuildFeature>,
        @SerialName("mfa_level")
        val mfaLevel: MFALevel,
        @SerialName("application_id")
        val applicationId: String? = null,
        @SerialName("widget_enabled")
        val widgetEnabled: Boolean? = null,
        @SerialName("widget_channel_id")
        val widgetChannelId: String? = null,
        @SerialName("system_channel_id")
        val systemChannelId: String? = null,
        @SerialName("system_channel_flags")
        val systemChannelFlags: SystemChannelFlags? = null,
        /**  The id of the channel in which a discoverable server's rules should be found **/
        @SerialName("rules_channel_id")
        val rulesChannelId: String? = null,
        @SerialName("joined_at")
        val joinedAt: String? = null,
        val large: Boolean? = null,
        val unavailable: Boolean? = null,
        @SerialName("member_count")
        val memberCount: Int? = null,
        @SerialName("voice_states")
        val voiceStates: List<DiscordVoiceState>? = null,
        val members: List<DiscordGuildMember>? = null,
        val channels: List<DiscordChannel>? = null,
        val presences: List<DiscordPresenceUpdateData>? = null,
        @SerialName("max_presences")
        val maxPresences: Int? = null,
        @SerialName("max_members")
        val maxMembers: Int? = null,
        @SerialName("vanity_url_code")
        val vanityUrlCode: String? = null,
        val description: String? = null,
        val banner: String? = null,
        @SerialName("premium_tier")
        val premiumTier: PremiumTier,
        @SerialName("premium_subscription_count")
        val premiumSubscriptionCount: Int? = null,
        @SerialName("preferred_locale")
        val preferredLocale: String,
        @SerialName("public_updates_channel_id")
        val publicUpdatesChannelId: String? = null,

        /**
         * Approximate number of members in this guild,
         * returned from the GET /guild/<id> endpoint when with_counts is true
         */
        @SerialName("approximate_member_count")
        val approximateMemberCount: Int? = null,

        /**
         * Approximate number of online members in this guild,
         * returned from the GET /guild/<id> endpoint when with_counts is true
         */
        @SerialName("approximate_presence_count")
        val approximatePresenceCount: Int? = null
)

@Serializable(with = GuildFeature.Companion::class)
enum class GuildFeature(val value: String) {
    Unknown(""),
    InviteSplash("INVITE_SPLASH"),
    VIPRegions("VIP_REGIONS"),
    VanityUrl("VANITY_URL"),
    Verified("VERIFIED"),
    Partnered("PARTNERED"),
    Public("PUBLIC"),
    Commerce("COMMERCE"),
    News("NEWS"),
    Discoverable("DISCOVERABLE"),
    Featureable("FEATURABLE"),
    AnimatedIcon("ANIMATED_ICON"),
    Banner("BANNER"),
    PublicDisabled("PUBLIC_DISABLED"),

    /**
     * guild has enabled the welcome screen
     */
    WelcomeScreenEnabled("WELCOME_SCREEN_ENABLED");

    @Serializer(forClass = GuildFeature::class)
    companion object : KSerializer<GuildFeature> {
        override val descriptor: SerialDescriptor
            get() = PrimitiveSerialDescriptor("feature", PrimitiveKind.STRING)

        override fun deserialize(decoder: Decoder): GuildFeature {
            val name = decoder.decodeString()
            return values().firstOrNull { it.value == name } ?: Unknown
        }

        override fun serialize(encoder: Encoder, obj: GuildFeature) {
            encoder.encodeString(obj.value)
        }
    }
}

@Serializable(with = SystemChannelFlags.Companion::class)
class SystemChannelFlags constructor(val code: Int) {

    operator fun contains(flag: SystemChannelFlags): Boolean {
        return this.code and flag.code == flag.code
    }

    @Serializer(forClass = SystemChannelFlags::class)
    companion object : KSerializer<SystemChannelFlags> {

        override val descriptor: SerialDescriptor
            get() = PrimitiveSerialDescriptor("system_channel_flags", PrimitiveKind.INT)

        override fun deserialize(decoder: Decoder): SystemChannelFlags {
            return SystemChannelFlags(decoder.decodeInt())
        }

        override fun serialize(encoder: Encoder, obj: SystemChannelFlags) {
            encoder.encodeInt(obj.code)
        }
    }

}

enum class SystemChannelFlag(val code: Int) {
    /** Suppress member join notifications. **/
    SuppressJoinNotifications(1.shl(0)),
    /** Suppress server boost notifications. **/
    SuppressPremiumSubscriptions(1.shl(1))
}

@Serializable
data class DiscordPartialGuild(
        val id: String,
        val name: String,
        val icon: String? = null,
        val owner: Boolean? = null,
        val permissions: Permissions? = null
)

@Serializable
data class DiscordGuildBan(
        @SerialName("guild_id")
        val guildId: String,
        val user: DiscordUser
)

@Serializable
data class DiscordGuildIntegrations(
        @SerialName("guild_id")
        val guildId: String
)

@Serializable
data class DiscordIntegrationAccount(val id: String,
                                     val name: String)

@Serializable
data class DiscordVoiceServerUpdateData(
        val token: String,
        @SerialName("guild_id")
        val guildId: String,
        val endpoint: String
)

@Serializable
data class DiscordWebhooksUpdateData(
        @SerialName("guild_id")
        val guildId: String,
        @SerialName("channel_id")
        val channelId: String
)

@Serializable
data class DiscordVoiceState(
        @SerialName("guild_id")
        val guildId: String? = null,
        @SerialName("channel_id")
        val channelId: String? = null,
        @SerialName("user_id")
        val userId: String,
        @SerialName("guild_member")
        val member: DiscordGuildMember? = null,
        @SerialName("session_id")
        val sessionId: String,
        val deaf: Boolean,
        val mute: Boolean,
        @SerialName("self_deaf")
        val selfDeaf: Boolean,
        @SerialName("self_mute")
        val selfMute: Boolean,
        @SerialName("self_stream")
        val selfStream: Boolean? = null,
        val suppress: Boolean
)

@Serializable(with = PremiumTier.PremiumTierSerializer::class)
enum class PremiumTier(val level: Int) {
    /** The default code for unknown values. */
    Unknown(Int.MIN_VALUE),
    None(0),
    One(1),
    Two(2),
    Three(3);

    @Serializer(forClass = PremiumTier::class)
    companion object PremiumTierSerializer : KSerializer<PremiumTier> {
        override val descriptor: SerialDescriptor
            get() = PrimitiveSerialDescriptor("premium_tier", PrimitiveKind.INT)

        override fun deserialize(decoder: Decoder): PremiumTier {
            val level = decoder.decodeInt()
            return values().firstOrNull { it.level == level } ?: Unknown
        }

        override fun serialize(encoder: Encoder, obj: PremiumTier) {
            encoder.encodeInt(obj.level)
        }

    }
}

@Serializable(with = DefaultMessageNotificationLevel.DefaultMessageNotificationLevelSerializer::class)
enum class DefaultMessageNotificationLevel(val code: Int) {
    /** The default code for unknown values. */
    Unknown(Int.MIN_VALUE),
    AllMessages(0),
    OnlyMentions(1);

    @Serializer(forClass = DefaultMessageNotificationLevel::class)
    companion object DefaultMessageNotificationLevelSerializer : KSerializer<DefaultMessageNotificationLevel> {
        override val descriptor: SerialDescriptor
            get() = PrimitiveSerialDescriptor("default_message_notifications", PrimitiveKind.INT)

        override fun deserialize(decoder: Decoder): DefaultMessageNotificationLevel {
            val code = decoder.decodeInt()
            return values().firstOrNull { it.code == code } ?: Unknown
        }

        override fun serialize(encoder: Encoder, obj: DefaultMessageNotificationLevel) {
            encoder.encodeInt(obj.code)
        }
    }

}

@Serializable(with = ExplicitContentFilter.ExplicitContentFilterSerializer::class)
enum class ExplicitContentFilter(val code: Int) {
    /** The default code for unknown values. */
    Unknown(Int.MIN_VALUE),
    Disabled(0),
    MembersWithoutRoles(1),
    AllMembers(2);

    @Serializer(forClass = ExplicitContentFilter::class)
    companion object ExplicitContentFilterSerializer : KSerializer<ExplicitContentFilter> {

        override val descriptor: SerialDescriptor
            get() = PrimitiveSerialDescriptor("explicit_content_filter", PrimitiveKind.INT)

        override fun deserialize(decoder: Decoder): ExplicitContentFilter {
            val code = decoder.decodeInt()
            return values().firstOrNull { it.code == code } ?: Unknown
        }

        override fun serialize(encoder: Encoder, obj: ExplicitContentFilter) {
            encoder.encodeInt(obj.code)
        }

    }
}

@Serializable(with = MFALevel.MFALevelSerializer::class)
enum class MFALevel(val code: Int) {
    /** The default code for unknown values. */
    Unknown(Int.MIN_VALUE),
    None(0),
    Elevated(1);

    @Serializer(forClass = MFALevel::class)
    object MFALevelSerializer : KSerializer<MFALevel> {

        override val descriptor: SerialDescriptor
            get() = PrimitiveSerialDescriptor("mfa_level", PrimitiveKind.INT)

        override fun deserialize(decoder: Decoder): MFALevel {
            val code = decoder.decodeInt()
            return values().firstOrNull { it.code == code } ?: Unknown
        }

        override fun serialize(encoder: Encoder, obj: MFALevel) {
            encoder.encodeInt(obj.code)
        }
    }
}

@Serializable(with = VerificationLevel.VerificationLevelSerializer::class)
enum class VerificationLevel(val code: Int) {
    /** The default code for unknown values. */
    Unknown(Int.MIN_VALUE),
    None(0),
    Low(1),
    Medium(2),
    High(3),
    VeryHigh(4);

    @Serializer(forClass = VerificationLevel::class)
    companion object VerificationLevelSerializer : KSerializer<VerificationLevel> {

        override val descriptor: SerialDescriptor
            get() = PrimitiveSerialDescriptor("verification_level", PrimitiveKind.INT)

        override fun deserialize(decoder: Decoder): VerificationLevel {
            val code = decoder.decodeInt()
            return values().firstOrNull { it.code == code } ?: Unknown
        }

        override fun serialize(encoder: Encoder, obj: VerificationLevel) {
            encoder.encodeInt(obj.code)
        }

    }
}

package com.gitlab.kordlib.common.entity

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class DiscordGuildPreview(
        /**
         * Guild id.
         */
        val id: String,
        /**
         * Guild name (2-100) characters.
         */
        val name: String,

        /**
         * Icon hash.
         */
        val icon: String? = null,

        /**
         * Splash hash.
         */
        val splash: String? = null,

        /**
         * Discovery splash hash.
         */
        @SerialName("discovery_splash")
        val discoverySplash: String? = null,

        /**
         * Custom guild emojis.
         */
        val emojis: List<DiscordEmoji>,

        /**
         * Enabled guild features.
         */
        val features: List<GuildFeature>,

        /**
         * Approximate number of members in this guild.
         */
        @SerialName("approximate_member_count")
        val approximateMemberCount: Int,

        /**
         * Approximate number of online members in this guild.
         */
        @SerialName("approximate_presence_count")
        val approximatePresenceCount: Int,

        /**
         * The description for the guild.
         */
        val description: String? = null
)

package com.gitlab.kordlib.common.entity

import kotlinx.serialization.*
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
import kotlinx.serialization.internal.IntDescriptor

@Serializable
data class DiscordMessage(
        val id: String,
        @SerialName("channel_id")
        val channelId: String,
        @SerialName("guild_id")
        val guildId: String? = null,
        val author: DiscordUser,
        val member: DiscordPartialGuildMember? = null,
        val content: String,
        val timestamp: String,
        @SerialName("edited_timestamp")
        val editedTimestamp: String? = null,
        val tts: Boolean,
        @SerialName("mention_everyone")
        val mentionEveryone: Boolean,
        val mentions: List<DiscordOptionallyMemberUser>,
        @SerialName("mention_roles")
        val mentionRoles: List<String>,
        val attachments: List<Attachment>,
        val embeds: List<Embed>,
        val reactions: List<Reaction>? = null,
        val nonce: String? = null,
        val pinned: Boolean,
        @SerialName("webhook_id")
        val webhookId: String? = null,
        val type: MessageType,
        val activity: MessageActivity? = null,
        val application: MessageApplication? = null,
        @SerialName("message_reference")
        val messageReference: MessageReference? = null,
        @SerialName("mention_channels")
        val mentionedChannels: List<MentionedChannel>? = null,
        val flags: Flags? = null
)
@Serializable
data class DiscordPartialMessage(
        val id: String,
        @SerialName("channel_id")
        val channelId: String,
        @SerialName("guild_id")
        val guildId: String? = null,
        val author: DiscordUser? = null,
        val member: DiscordPartialGuildMember? = null,
        val content: String? = null,
        val timestamp: String? = null,
        @SerialName("edited_timestamp")
        val editedTimestamp: String? = null,
        val tts: Boolean? = null,
        @SerialName("mention_everyone")
        val mentionEveryone: Boolean? = null,
        val mentions: List<DiscordOptionallyMemberUser>? = null,
        @SerialName("mention_roles")
        val mentionRoles: List<String> ? = null,
        val attachments: List<Attachment> ? = null,
        val embeds: List<Embed> ? = null,
        val reactions: List<Reaction>? = null,
        val nonce: String? = null,
        val pinned: Boolean? = null,
        @SerialName("webhook_id")
        val webhookId: String? = null,
        val type: MessageType? = null,
        val activity: MessageActivity? = null,
        val application: MessageApplication? = null,
        @SerialName("message_reference")
        val messageReference: MessageReference? = null,
        @SerialName("mention_channels")
        val mentionedChannels: List<MentionedChannel>? = null,
        val flags: Flags? = null
)

@Serializable
data class MessageReference(
        @SerialName("message_id")
        val id: String? = null,
        @SerialName("channel_id")
        val channelId: String,
        @SerialName("guild_id")
        val guildId: String? = null
)

@Serializable
data class MentionedChannel(
        val id: String,
        @SerialName("guild_id")
        val guildId: String,
        val name: String,
        val type: MessageType
)

enum class Flag(val code: Int) {
    CrossPosted(1),
    IsCrossPost(2),
    SuppressEmbeds(4);
}

@Serializable(with = Flags.FlagsSerializer::class)
data class Flags internal constructor(val code: Int) {

    val flags = Flag.values().filter { code and it.code != 0 }

    operator fun contains(flag: Flag) = flag in flags

    operator fun plus(flags: Flags): Flags = when {
        code and flags.code == flags.code -> this
        else -> Flags(this.code or flags.code)
    }

    operator fun minus(flag: Flag): Flags = when {
        code and flag.code == flag.code -> Flags(code xor flag.code)
        else -> this
    }

    inline fun copy(block: FlagsBuilder.() -> Unit): Flags {
        val builder = FlagsBuilder(code)
        builder.apply(block)
        return builder.flags()
    }

    @Serializer(forClass = Flags::class)
    companion object FlagsSerializer : DeserializationStrategy<Flags> {

        inline operator fun invoke(builder: FlagsBuilder.() -> Unit): Flags {
            return FlagsBuilder().apply(builder).flags()
        }

        override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("flags", PrimitiveKind.INT)

        override fun deserialize(decoder: Decoder): Flags {
            val flags = decoder.decodeInt()
            return Flags(flags)
        }

    }

    class FlagsBuilder(internal var code: Int = 0) {
        operator fun Flag.unaryPlus() {
            this@FlagsBuilder.code = this@FlagsBuilder.code or code
        }

        operator fun Flag.unaryMinus() {
            if (this@FlagsBuilder.code and code == code) {
                this@FlagsBuilder.code = this@FlagsBuilder.code xor code
            }
        }

        fun flags() = Flags(code)
    }

}

@Serializable
data class Attachment(
        val id: String,
        val filename: String? = null,
        val size: Int,
        val url: String,
        @SerialName("proxy_url")
        val proxyUrl: String,
        val height: Int? = null,
        val width: Int? = null
)

@Serializable
data class Embed(
        val title: String? = null,
        val type: String? = null,
        val description: String? = null,
        val url: String? = null,
        val timestamp: String? = null,
        val color: Int? = null,
        val footer: Footer? = null,
        val image: Image? = null,
        val thumbnail: Thumbnail? = null,
        val video: Video? = null,
        val provider: Provider? = null,
        val author: Author? = null,
        val fields: List<Field>? = null
) {
    @Serializable
    data class Footer(
            val text: String,
            @SerialName("icon_url")
            val iconUrl: String? = null,
            @SerialName("proxy_icon_url")
            val proxyIconUrl: String? = null
    )

    @Serializable
    data class Image(
            val url: String? = null,
            @SerialName("proxy_url")
            val proxyUrl: String? = null,
            val height: Int? = null,
            val width: Int? = null
    )

    @Serializable
    data class Thumbnail(
            val url: String? = null,
            @SerialName("proxy_url")
            val proxyUrl: String? = null,
            val height: Int? = null,
            val width: Int? = null
    )

    @Serializable
    data class Video(val url: String? = null, val height: Int? = null, val width: Int? = null)

    @Serializable
    data class Provider(val name: String? = null, val url: String? = null)

    @Serializable
    data class Author(
            val name: String? = null,
            val url: String? = null,
            @SerialName("icon_url")
            val iconUrl: String? = null,
            @SerialName("proxy_icon_url")
            val proxyIconUrl: String? = null
    )

    @Serializable
    data class Field(val name: String, val value: String, val inline: Boolean? = null)
}

@Serializable
data class Reaction(
        val count: Int,
        val me: Boolean,
        val emoji: DiscordEmoji
)

@Serializable
data class MessageActivity(val type: Int, @SerialName("party_id") val partyId: String? = null)

@Serializable
data class MessageApplication(
        val id: String,
        @SerialName("cover_image")
        val coverImage: String? = null,
        val description: String,
        val icon: String,
        val name: String
)

@Serializable
data class DeletedMessage(
        val id: String,
        @SerialName("channel_id")
        val channelId: String,
        @SerialName("guild_id")
        val guildId: String? = null
)

@Serializable
data class BulkDeleteData(
        val ids: List<String>,
        @SerialName("channel_id")
        val channelId: String,
        @SerialName("guild_id")
        val guildId: String? = null
)

@Serializable
data class MessageReaction(
        @SerialName("user_id")
        val userId: String,
        @SerialName("channel_id")
        val channelId: String,
        @SerialName("message_id")
        val messageId: String,
        @SerialName("guild_id")
        val guildId: String? = null,
        val member: DiscordGuildMember? = null,
        val emoji: DiscordPartialEmoji
)

@Serializable
data class AllRemovedMessageReactions(
        @SerialName("channel_id")
        val channelId: String,
        @SerialName("message_id")
        val messageId: String,
        @SerialName("guild_id")
        val guildId: String? = null
)

@Serializable(with = MessageType.MessageTypeSerializer::class)
enum class MessageType(val code: Int) {
    /** The default code for unknown values. */
    Unknown(Int.MIN_VALUE),
    Default(0),
    RecipientAdd(1),
    RecipientRemove(2),
    Call(3),
    ChannelNameChange(4),
    ChannelIconChange(5),
    ChannelPinnedMessage(6),
    GuildMemberJoin(7),
    UserPremiumGuildSubscription(8),
    UserPremiumGuildSubscriptionTierOne(9),
    UserPremiumGuildSubscriptionTwo(10),
    UserPremiumGuildSubscriptionThree(11),
    ChannelFollowAdd(12),
    GuildDiscoveryDisqualified(14),
    @Suppress("SpellCheckingInspection")
    GuildDiscoveryRequalified(15);

    @Serializer(forClass = MessageType::class)
    companion object MessageTypeSerializer : KSerializer<MessageType> {

        override val descriptor: SerialDescriptor
            get() = PrimitiveSerialDescriptor("type", PrimitiveKind.INT)

        override fun deserialize(decoder: Decoder): MessageType {
            val code = decoder.decodeInt()
            return values().firstOrNull { it.code == code } ?: Unknown
        }

        override fun serialize(encoder: Encoder, obj: MessageType) {
            encoder.encodeInt(obj.code)
        }
    }
}

package com.gitlab.kordlib.common.entity

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class DiscordRole(
        val id: String,
        val name: String,
        val color: Int,
        val hoist: Boolean,
        val position: Int,
        val permissions: Permissions,
        val managed: Boolean,
        val mentionable: Boolean
)

@Serializable
data class DiscordAuditLogRoleChange(
        val id: String,
        val name: String? = null,
        val color: Int? = null,
        val hoist: Boolean? = null,
        val position: Int? = null,
        val permissions: Permissions? = null,
        val managed: Boolean? = null,
        val mentionable: Boolean? = null
)

@Serializable
data class DiscordGuildRole(
        @SerialName("guild_id")
        val guildId: String,
        val role: DiscordRole
)

@Serializable
data class DiscordDeletedGuildRole(
        @SerialName("guild_id")
        val guildId: String,
        @SerialName("role_id")
        val id: String
)

package com.gitlab.kordlib.common.entity

import kotlinx.serialization.*
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.*

/**
 * An instance of a [Discord shard](https://discord.com/developers/docs/topics/gateway#sharding).
 */
@Serializable
data class DiscordShard(val index: Int, val count: Int) {

    @Serializer(forClass = DiscordShard::class)
    companion object : KSerializer<DiscordShard> {

        override fun serialize(encoder: Encoder, obj: DiscordShard) {
            val array = buildJsonArray {
                add(JsonPrimitive(obj.index))
                add(JsonPrimitive(obj.count))
            }

            encoder.encodeSerializableValue(JsonArray.serializer(), array)
        }

        override fun deserialize(decoder: Decoder): DiscordShard {
            val array = JsonArray.serializer().deserialize(decoder)
            val index = array[0].jsonPrimitive.int
            val count = array[1].jsonPrimitive.int
            return DiscordShard(index, count)
        }

    }

}

package com.gitlab.kordlib.common.entity

import kotlinx.serialization.*
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
import kotlinx.serialization.internal.IntDescriptor
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

@Serializable
data class DiscordUser(
        val id: String,
        val username: String,
        val discriminator: String,
        val avatar: String? = null,
        val bot: Boolean? = null,
        @SerialName("mfa_enable")
        val mfaEnable: Boolean? = null,
        val locale: String? = null,
        val flags: UserFlags? = null,
        @SerialName("premium_type")
        val premiumType: Premium? = null,
        val verified: Boolean? = null,
        val email: String? = null
)

enum class UserFlag(val code: Int) {
    None(0),
    DiscordEmployee(1 shl 0),
    DiscordPartner(1 shl 1),
    HypeSquad(1 shl 2),
    BugHunterLevel1(1 shl 3),
    HouseBravery(1 shl 6),
    HouseBrilliance(1 shl 7),
    HouseBalance(1 shl 8),
    EarlySupporter(1 shl 9),
    TeamUser(1 shl 10),
    System(1 shl 12),
    BugHunterLevel2(1 shl 14),
    VerifiedBot(1 shl 16),
    VerifiedBotDeveloper(1 shl 17)
}

@Serializable(with = UserFlags.UserFlagsSerializer::class)
data class UserFlags constructor(val code: Int) {

    val flags = UserFlag.values().filter { code and it.code != 0 }

    operator fun contains(flag: UserFlag) = flag in flags

    operator fun plus(flags: UserFlags): UserFlags = when {
        code and flags.code == flags.code -> this
        else -> UserFlags(this.code or flags.code)
    }

    operator fun minus(flag: UserFlag): UserFlags = when {
        code and flag.code == flag.code -> UserFlags(code xor flag.code)
        else -> this
    }

    @OptIn(ExperimentalContracts::class)
    inline fun copy(block: UserFlagsBuilder.() -> Unit): UserFlags {
        contract {
            callsInPlace(block, InvocationKind.EXACTLY_ONCE)
        }
        val builder = UserFlagsBuilder(code)
        builder.apply(block)
        return builder.flags()
    }

    @Serializer(forClass = UserFlags::class)
    companion object UserFlagsSerializer : DeserializationStrategy<UserFlags> {

        inline operator fun invoke(builder: UserFlagsBuilder.() -> Unit): UserFlags {
            return UserFlagsBuilder().apply(builder).flags()
        }

        override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("userFlag", PrimitiveKind.INT)

        override fun deserialize(decoder: Decoder): UserFlags {
            val flags = decoder.decodeInt()
            return UserFlags(flags)
        }

    }

    class UserFlagsBuilder(internal var code: Int = 0) {
        operator fun UserFlag.unaryPlus() {
            this@UserFlagsBuilder.code = this@UserFlagsBuilder.code or code
        }

        operator fun UserFlag.unaryMinus() {
            if (this@UserFlagsBuilder.code and code == code) {
                this@UserFlagsBuilder.code = this@UserFlagsBuilder.code xor code
            }
        }

        fun flags() = UserFlags(code)
    }

}

@Serializable
data class DiscordOptionallyMemberUser(
        val id: String,
        val username: String,
        val discriminator: String,
        val avatar: String? = null,
        val bot: Boolean? = null,
        @SerialName("mfa_enable")
        val mfaEnable: Boolean? = null,
        val locale: String? = null,
        val flags: Int? = null,
        @SerialName("premium_type")
        val premiumType: Premium? = null,
        val member: DiscordPartialGuildMember? = null
)

@Serializable(with = Premium.PremiumSerializer::class)
enum class Premium(val code: Int) {
    /** The default code for unknown values. */
    Unknown(Int.MIN_VALUE),
    None(0),
    NitroClassic(1),
    Nitro(2);

    @Serializer(forClass = Premium::class)
    companion object PremiumSerializer : KSerializer<Premium> {
        override val descriptor: SerialDescriptor
            get() = PrimitiveSerialDescriptor("premium_type", PrimitiveKind.INT)

        override fun deserialize(decoder: Decoder): Premium {
            val code = decoder.decodeInt()
            return values().firstOrNull { it.code == code } ?: Unknown
        }

        override fun serialize(encoder: Encoder, value: Premium) {
            encoder.encodeInt(value.code)
        }
    }
}

package com.gitlab.kordlib.common.entity

import kotlinx.serialization.*
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
import kotlinx.serialization.internal.IntDescriptor

@Serializable
data class DiscordWebhook(
        val id: String,
        val type: WebhookType,
        @SerialName("guild_id")
        val guildId: String? = null,
        @SerialName("channel_id")
        val channelId: String,
        val user: DiscordUser? = null,
        val name: String? = null,
        val avatar: String? = null,
        val token: String? = null
)

@Serializable(with = WebhookType.WebhookTypeSerializer::class)
enum class WebhookType(val code: Int) {
    Unknown(-1),
    Incoming(1),
    ChannelOrder(2);

    @Serializer(forClass = MessageType::class)
    companion object WebhookTypeSerializer : KSerializer<WebhookType> {

        override val descriptor: SerialDescriptor
            get() = PrimitiveSerialDescriptor("type", PrimitiveKind.INT)

        override fun deserialize(decoder: Decoder): WebhookType {
            val code = decoder.decodeInt()
            return values().firstOrNull { it.code == code } ?: Unknown
        }

        override fun serialize(encoder: Encoder, obj: WebhookType) {
            encoder.encodeInt(obj.code)
        }
    }
}

package com.gitlab.kordlib.common.entity

import kotlinx.serialization.*
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 = TargetUserType.TargetUserTypeSerializer::class)
enum class TargetUserType(val code: Int) {
    /** The default code for unknown values. */
    Unknown(Int.MIN_VALUE),
    STREAM(1);

    @Serializer(forClass = TargetUserType::class)
    companion object TargetUserTypeSerializer : KSerializer<TargetUserType> {
        override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("TargetUserType", PrimitiveKind.INT)

        override fun deserialize(decoder: Decoder): TargetUserType {
            val code = decoder.decodeInt()

            return values().firstOrNull { it.code == code } ?: Unknown
        }

        override fun serialize(encoder: Encoder, value: TargetUserType) {
            encoder.encodeInt(value.code)
        }
    }

}

package com.gitlab.kordlib.common.entity

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class DiscordGuildMember(
        val user: DiscordUser? = null,
        val nick: String? = null,
        val roles: List<String>,
        @SerialName("joined_at")
        val joinedAt: String,
        @SerialName("premium_since")
        val premiumSince: String? = null,
        val deaf: Boolean,
        val mute: Boolean
)

@Serializable
data class DiscordAddedGuildMember(
        val user: DiscordUser? = null,
        val nick: String? = null,
        val roles: List<String>,
        @SerialName("joined_at")
        val joinedAt: String,
        @SerialName("premium_since")
        val premiumSince: String? = null,
        val deaf: Boolean,
        val mute: Boolean,
        @SerialName("guild_id")
        val guildId: String
)

@Serializable
data class DiscordRemovedGuildMember(
        @SerialName("guild_id")
        val guildId: String,
        val user: DiscordUser
)

@Serializable
data class DiscordUpdatedGuildMember(
        @SerialName("guild_id")
        val guildId: String,
        val roles: List<String>,
        val user: DiscordUser,
        val nick: String? = null,
        @SerialName("premium_since")
        val premiumSince: String? = null
)

@Serializable
data class DiscordPartialGuildMember(
        val nick: String? = null,
        val roles: List<String>,
        @SerialName("joined_at")
        val joinedAt: String,
        val deaf: Boolean,
        val mute: Boolean,
        @SerialName("premium_since")
        val premiumSince: String? = null
)

package com.gitlab.kordlib.common.entity

import kotlinx.serialization.*
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
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

@Serializable(with = Permissions.Companion::class)
class Permissions constructor(val code: Int) {

    operator fun plus(permission: Permission): Permissions = Permissions(this.code or permission.code)

    operator fun minus(permission: Permission): Permissions = Permissions(code xor (code and permission.code))

    operator fun contains(permission: Permission): Boolean {
        return this.code and permission.code == permission.code
    }

    @OptIn(ExperimentalContracts::class)
    inline fun copy(block: PermissionsBuilder.() -> Unit): Permissions {
        contract {
            callsInPlace(block, InvocationKind.EXACTLY_ONCE)
        }
        val builder = PermissionsBuilder(code)
        builder.apply(block)
        return builder.permissions()
    }

    @Serializer(forClass = Permissions::class)
    companion object : KSerializer<Permissions> {

        inline operator fun invoke(block: PermissionsBuilder.() -> Unit = {}): Permissions {
            val builder = PermissionsBuilder()
            builder.apply(block)
            return builder.permissions()
        }

        override val descriptor: SerialDescriptor
            get() = PrimitiveSerialDescriptor("permission", PrimitiveKind.INT)

        override fun deserialize(decoder: Decoder): Permissions {
            return Permissions(decoder.decodeInt())
        }

        override fun serialize(encoder: Encoder, value: Permissions) {
            encoder.encodeInt(value.code)
        }
    }

    class PermissionsBuilder(internal var code: Int = 0) {
        operator fun Permissions.unaryPlus() {
            this@PermissionsBuilder.code = this@PermissionsBuilder.code or code
        }

        operator fun Permissions.unaryMinus() {
            this@PermissionsBuilder.code = this@PermissionsBuilder.code xor (this@PermissionsBuilder.code and code)
        }

        operator fun Permission.unaryPlus() {
            this@PermissionsBuilder.code = this@PermissionsBuilder.code or code
        }

        operator fun Permission.unaryMinus() {
            this@PermissionsBuilder.code = this@PermissionsBuilder.code xor (this@PermissionsBuilder.code and code)
        }

        fun permissions() = Permissions(code)
    }

}

enum class Permission(val code: Int = 0) {
    CreateInstantInvite(0x00000001),
    KickMembers(0x00000002),
    BanMembers(0x00000004),
    Administrator(0x00000008),
    ManageChannels(0x00000010),
    ManageGuild(0x00000020),
    AddReactions(0x00000040),
    ViewAuditLog(0x00000080),
    ViewChannel(0x00000400),
    SendMessages(0x00000800),
    SendTTSMessages(0x00001000),
    ManageMessages(0x00002000),
    EmbedLinks(0x00004000),
    AttachFiles(0x00008000),
    ReadMessageHistory(0x00010000),
    MentionEveryone(0x00020000),
    UseExternalEmojis(0x00040000),
    ViewGuildInsights(0x00080000),
    Connect(0x00100000),
    Speak(0x00200000),
    MuteMembers(0x00400000),
    DeafenMembers(0x00800000),
    MoveMembers(0x01000000),
    UseVAD(0x02000000),
    PrioritySpeaker(0x00000100),
    ChangeNickname(0x04000000),
    ManageNicknames(0x08000000),
    ManageRoles(0x10000000),
    ManageWebhooks(0x20000000),
    ManageEmojis(0x40000000),
    All(0x7FFFFDFF)
}

package com.gitlab.kordlib.common.entity

import kotlinx.serialization.*
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
import kotlinx.serialization.json.JsonElement

@Serializable
data class DiscordPresenceUpdateData(
        val user: DiscordPresenceUser,
        val roles: List<String>? = null,
        val game: DiscordActivity? = null,
        @SerialName("guild_id")
        val guildId: String? = null, //don't trust the docs
        val status: Status,
        val activities: List<DiscordActivity>,
        @SerialName("client_status")
        val clientStatus: DiscordClientStatus
)

@Serializable
data class DiscordPresenceUser(
        val id: String,
        val username: JsonElement? = null,
        val discriminator: JsonElement? = null,
        val avatar: JsonElement? = null,
        val bot: JsonElement? = null,
        @SerialName("mfa_enable")
        val mfaEnable: JsonElement? = null,
        val locale: JsonElement? = null,
        val flags: JsonElement? = null,
        @SerialName("premium_type")
        val premiumType: JsonElement? = null,
        val verified: JsonElement? = null,
        val email: JsonElement? = null
)

@Serializable
data class DiscordClientStatus(val desktop: Status? = null, val mobile: Status? = null, val web: Status? = null)

@Serializable(with = Status.StatusSerializer::class)
enum class Status {
        /** The default code for unknown values. */
        Unknown,
        Online, DnD, Idle, Invisible, Offline;

        @Serializer(forClass = Status::class)
        companion object StatusSerializer : KSerializer<Status> {
                override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Status", PrimitiveKind.STRING)

                override fun deserialize(decoder: Decoder): Status {
                        val name = decoder.decodeString()
                        return values().firstOrNull { it.name.toLowerCase() == name } ?: Unknown
                }

                override fun serialize(encoder: Encoder, value: Status) {
                        encoder.encodeString(value.name.toLowerCase())
                }
        }
}

package com.gitlab.kordlib.common.entity

import java.time.Instant
import kotlin.time.Duration
import kotlin.time.TimeMark
import kotlin.time.toKotlinDuration

/**
 * A unique identifier for entities [used by discord](https://discord.com/developers/docs/reference#snowflakes).
 */
inline class Snowflake(val longValue: Long) : Comparable<Snowflake> {
    constructor(value: String) : this(value.toLong())

    val value get() = longValue.toString()

    val timeStamp: Instant get() = Instant.ofEpochMilli(discordEpoch + (longValue shr 22))

    val timeMark: TimeMark get() = SnowflakeMark(longValue shr 22)

    override fun compareTo(other: Snowflake): Int = longValue.shr(22).compareTo(other.longValue.shr(22))

    companion object {
        const val discordEpoch = 1420070400000L
    }
}

private class SnowflakeMark(val epochMilliseconds: Long) : TimeMark() {

    override fun elapsedNow(): Duration =
            java.time.Duration.between(Instant.ofEpochMilli(epochMilliseconds), Instant.now()).toKotlinDuration()

}

package com.gitlab.kordlib.common.entity

import kotlinx.serialization.*
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

/**
 * The raw developer team data gotten from the API.
 */
@Serializable
data class DiscordTeam(
        /**
         * The unique ID of this team.
         */
        val id: String,
        /**
         * The hash of this team's icon.
         */
        val icon: String?,
        /**
         * A collection of all members of this team.
         */
        val members: List<DiscordTeamMember>,
        /**
         * The ID of the user that owns the team.
         */
        @SerialName("owner_user_id")
        val ownerUserId: String
)

/**
 * The state of membership on a Discord developer team.
 */
@Serializable(with = TeamMembershipState.TeamMembershipStateSerializer::class)
enum class TeamMembershipState(val value: Int) {
    /**
     * Unknown membership state.
     */
    Unknown(-1),

    /**
     * The user has been invited.
     */
    Invited(1),

    /**
     * The user has accepted the invitation.
     */
    Accepted(2);

    @OptIn(ExperimentalSerializationApi::class)
    @Serializer(forClass = TeamMembershipState::class)
    companion object TeamMembershipStateSerializer : KSerializer<TeamMembershipState> {

        override val descriptor: SerialDescriptor
            get() = PrimitiveSerialDescriptor("membership_state", PrimitiveKind.INT)

        override fun deserialize(decoder: Decoder): TeamMembershipState {
            val code = decoder.decodeInt()
            return values().firstOrNull { it.value == code } ?: Unknown
        }

        override fun serialize(encoder: Encoder, value: TeamMembershipState) {
            encoder.encodeInt(value.value)
        }
    }
}

/**
 * The raw developer team member data gotten from the API.
 */
@Serializable
class DiscordTeamMember(
        /**
         * An integer enum representing the state of membership of this user.
         * `1` means the user has been invited and `2` means the user has accepted the invitation.
         */
        @SerialName("membership_state")
        val membershipState: TeamMembershipState,
        /**
         * A collection of permissions granted to this member.
         * At the moment, this collection will only have one element: `*`, meaning the member has all permissions.
         * This is because right now there are no other permissions. Read mode [here](https://discord.com/developers/docs/topics/teams#data-models-team-members-object)
         */
        val permissions: List<String>,
        /**
         * The unique ID that this member belongs to.
         */
        @SerialName("team_id")
        val teamId: String,
        /**
         * Partial user data containing only the ID, username, discriminator and avatar.
         */
        val user: DiscordUser
)

package com.gitlab.kordlib.common.exception

/**
 * Signals that some kind of exception occurred when attempting to interact with an entity.
 */
abstract class RequestException : Exception {

    constructor(message: String) : super(message)
    constructor(cause: Throwable) : super(cause)
    constructor(message: String, cause: Throwable) : super(message, cause)

}

package com.gitlab.kordlib.common.ratelimit

import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.time.Clock
import kotlin.time.Duration
import kotlin.time.milliseconds
import kotlin.time.toKotlinDuration
import java.time.Duration as JavaDuration

/**
 * A rate limiter that supplies a given [capacity] of permits for each [interval](refillIntervalMillis).
 * Exceeding this [capacity] will result in a suspend until the next [interval](refillIntervalMillis).
 *
 * @param capacity The maximum amount of permits that are given for each permit.
 * @param refillInterval The duration between permit refills.
 */
class BucketRateLimiter(private val capacity: Int, private val refillInterval: Duration, private val clock: Clock = Clock.systemUTC()) : RateLimiter {

    private val mutex = Mutex()

    private var count = 0
    private var nextInterval = 0L

    init {
        require(capacity > 0) { "capacity must be a positive number" }
        require(refillInterval.isPositive()) { "refill interval must be positive" }
    }

    private val isNextInterval get() = nextInterval <= clock.millis()

    private val isAtCapacity get() = count == capacity

    private fun resetState() {
        count = 0
        nextInterval = clock.millis() + refillInterval.inMilliseconds.toLong()
    }

    private suspend fun delayUntilNextInterval() {
        val delay = nextInterval - clock.millis()
        kotlinx.coroutines.delay(delay)
    }

    override suspend fun consume() = mutex.withLock {
        if (isNextInterval) resetState()

        if (isAtCapacity) {
            delayUntilNextInterval()
            resetState()
        }

        count += 1
    }
}

package com.gitlab.kordlib.common.ratelimit

import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

/**
 * A rate limiter that limits the amount of [consume] invocations that can be made over a certain period.
 */
interface RateLimiter {

    /**
     * Acquires a permit for a single action. Suspends if necessary until the permit would not exceed
     * the maximum frequency of permits.
     */
    suspend fun consume()
}

/**
 * Acquires a permit for a single [action]. Suspends if necessary until the permit would not exceed
 * the maximum frequency of permits.
 *
 * @param action The action that correlates to a single permit.
 */
@OptIn(ExperimentalContracts::class)
suspend inline fun <T> RateLimiter.consume(action: () -> T): T {
    contract {
        callsInPlace(action, InvocationKind.EXACTLY_ONCE)
    }
    consume()
    return action()
}

package com.gitlab.kordlib.core.behavior

import com.gitlab.kordlib.cache.api.query
import com.gitlab.kordlib.common.annotation.KordPreview
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.cache.data.*
import com.gitlab.kordlib.core.entity.*
import com.gitlab.kordlib.core.entity.channel.*
import com.gitlab.kordlib.core.exception.EntityNotFoundException
import com.gitlab.kordlib.core.sorted
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy.Companion.rest
import com.gitlab.kordlib.core.supplier.getChannelOf
import com.gitlab.kordlib.core.supplier.getChannelOfOrNull
import com.gitlab.kordlib.rest.builder.ban.BanCreateBuilder
import com.gitlab.kordlib.rest.builder.channel.*
import com.gitlab.kordlib.rest.builder.guild.EmojiCreateBuilder
import com.gitlab.kordlib.rest.builder.guild.GuildModifyBuilder
import com.gitlab.kordlib.rest.builder.role.RoleCreateBuilder
import com.gitlab.kordlib.rest.builder.role.RolePositionsModifyBuilder
import com.gitlab.kordlib.rest.json.request.CurrentUserNicknameModifyRequest
import com.gitlab.kordlib.rest.request.RestRequestException
import com.gitlab.kordlib.rest.service.createCategory
import com.gitlab.kordlib.rest.service.createNewsChannel
import com.gitlab.kordlib.rest.service.createTextChannel
import com.gitlab.kordlib.rest.service.createVoiceChannel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import java.util.*
import com.gitlab.kordlib.rest.service.RestClient
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

/**
 * The behavior of a [Discord Guild](https://discord.com/developers/docs/resources/guild).
 */
interface GuildBehavior : Entity, Strategizable {
    /**
     * Requests to get all present bans for this guild.
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    val bans: Flow<Ban>
        get() = supplier.getGuildBans(id)

    /**
     * Requests to get all present webhooks for this guild.
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    val webhooks: Flow<Webhook>
        get() = supplier.getGuildWebhooks(id)

    /**
     * Requests to get all present channels in this guild in an unspecified order,
     * call [sorted] to get a consistent order.
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    val channels: Flow<GuildChannel>
        get() = supplier.getGuildChannels(id)

    /**
     * Requests to get the integrations of this guild.
     */
    val integrations: Flow<Integration>
        get() = flow {
            kord.rest.guild.getGuildIntegrations(id.value).forEach {
                emit(Integration(IntegrationData.from(id.longValue, it), kord, supplier))
            }
        }

    /**
     * Requests to get all present presences of this guild.
     *
     * This property is not resolvable through REST and will always use [KordCache] instead.
     */
    val presences: Flow<Presence>
        get() = kord.cache.query<PresenceData> { PresenceData::guildId eq id.longValue }
                .asFlow()
                .map { Presence(it, kord) }

    /**
     * Requests to get all present members in this guild.
     *
     * Unrestricted consumption of the returned [Flow] is a potentially performance intensive operation, it is thus recommended
     * to limit the amount of messages requested by using [Flow.take], [Flow.takeWhile] or other functions that limit the amount
     * of messages requested.
     *
     * ```kotlin
     *  guild.members.first { it.displayName == targetName }
     * ```
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    val members: Flow<Member>
        get() = supplier.getGuildMembers(id)

    /**
     * Requests to get the present voice regions for this guild.
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    val regions: Flow<Region>
        get() = supplier.getGuildVoiceRegions(id)

    /**
     * Requests to get all present roles in the guild.
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    val roles: Flow<Role>
        get() = supplier.getGuildRoles(id)

    /**
     * Requests to get the present voice states of this guild.
     *
     * This property is not resolvable through REST and will always use [KordCache] instead.
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    val voiceStates: Flow<VoiceState>
        get() = kord.cache
                .query<VoiceStateData> { VoiceStateData::guildId eq id.longValue }
                .asFlow()
                .map { VoiceState(it, kord) }

    /**
     * Requests to get the this behavior as a [Guild].
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the guild wasn't present.
     */
    suspend fun asGuild(): Guild = supplier.getGuild(id)

    /**
     * Requests to get this behavior as a [Guild],
     * returns null if the guild isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun asGuildOrNull(): Guild? = supplier.getGuildOrNull(id)

    /**
     * Requests to delete this guild.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun delete() = kord.rest.guild.deleteGuild(id.value)

    /**
     * Requests to leave this guild.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun leave() = kord.rest.user.leaveGuild(id.value)

    /**
     * Requests to get the [Member] represented by the [userId].
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the member wasn't present.
     */
    suspend fun getMember(userId: Snowflake): Member = supplier.getMember(id, userId)

    /**
     * Requests to get the [Member] represented by the [userId],
     * returns null if the [Member] isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getMemberOrNull(userId: Snowflake): Member? = supplier.getMemberOrNull(id, userId)

    /**
     * Requests to get the [Role] represented by the [roleId].
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the [Role] wasn't present.
     */
    suspend fun getRole(roleId: Snowflake): Role = supplier.getRole(guildId = id, roleId = roleId)

    /**
     * Requests to get the [Role] represented by the [roleId],
     * returns null if the [Role] isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getRoleOrNull(roleId: Snowflake): Role? = supplier.getRoleOrNull(guildId = id, roleId = roleId)

    /**
     * Requests to get the [Invite] represented by the [code].
     *
     *
     * This property is not resolvable through cache and will always use the [RestClient] instead.
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the [Invite] wasn't present.
     */
    suspend fun getInvite(code: String, withCounts: Boolean = true): Invite =
            kord.with(rest).getInvite(code, withCounts)

    /**
     * Requests to get the [Invite] represented by the [code],
     * returns null if the [Invite] isn't present.
     *
     * This property is not resolvable through cache and will always use the [RestClient] instead.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getInviteOrNull(code: String, withCounts: Boolean = true): Invite? =
            kord.with(rest).getInviteOrNull(code, withCounts)

    /**
     *  Requests to change the nickname of the bot in this guild, passing `null` will remove it.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun modifySelfNickname(newNickName: String?): String {
        return kord.rest.guild.modifyCurrentUserNickname(id.value, CurrentUserNicknameModifyRequest(newNickName))
    }

    /**
     * Requests to kick the given [userId].
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun kick(userId: Snowflake, reason: String? = null) {
        kord.rest.guild.deleteGuildMember(guildId = id.value, userId = userId.value, reason = reason)
    }

    /**
     * Requests to get the [Ban] of the [User] represented by the [userId].
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the [Ban] wasn't present.
     */
    suspend fun getBan(userId: Snowflake): Ban = supplier.getGuildBan(id, userId)

    /**
     * Requests to get the [Ban] of the [User] represented by the [userId],
     * returns null if the [Ban] isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getBanOrNull(userId: Snowflake): Ban? = supplier.getGuildBanOrNull(id, userId)

    /**
     * Requests to get the [GuildChannel] represented by the [channelId].
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the [GuildChannel] wasn't present.
     * @throws [ClassCastException] if the channel is not a [GuildChannel].
     * @throws [IllegalArgumentException] if the channel is not part of this guild.
     */
    suspend fun getChannel(channelId: Snowflake) : GuildChannel {
        val channel = supplier.getChannelOf<GuildChannel>(channelId)
        require(channel.guildId == this.id) { "channel ${channelId.value} is not in guild ${this.id.value}" }
        return channel
    }

    /**
     * Requests to get the [GuildChannel] represented by the [channelId],
     * returns null if the [GuildChannel] isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [ClassCastException] if the channel is not a [GuildChannel].
     * @throws [IllegalArgumentException] if the channel is not part of this guild.
     */
    suspend fun getChannelOrNull(channelId: Snowflake) : GuildChannel? {
        val channel = supplier.getChannelOfOrNull<GuildChannel>(channelId) ?: return null
        require(channel.guildId == this.id) { "channel ${channelId.value} is not in guild ${this.id.value}" }
        return channel
    }

    /**
     * Requests to unban the given [userId].
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    @Deprecated("unBan is a typo", ReplaceWith("unban"))
    suspend fun unBan(userId: Snowflake) {
        kord.rest.guild.deleteGuildBan(guildId = id.value, userId = userId.value)
    }

    /**
     * Requests to unban the given [userId].
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun unban(userId: Snowflake) {
        kord.rest.guild.deleteGuildBan(guildId = id.value, userId = userId.value)
    }

    /**
     * Returns the preview of this guild. The bot does not need to present in this guild
     * for this to complete successfully.
     *
     * This property is not resolvable through cache and will always use the [RestClient] instead.
     *
     * @throws RequestException if the guild does not exist or is not public.
     * @throws [EntityNotFoundException] if the preview was not found.
     */
    suspend fun getPreview(): GuildPreview = kord.with(rest).getGuildPreview(id)

    /**
     * Returns the preview of this guild. The bot does not need to present in this guild
     * for this to complete successfully. Returns null if the preview doesn't exist.
     *
     * This property is not resolvable through cache and will always use the [RestClient] instead.
     *
     * @throws RequestException if the guild does not exist or is not public.
     */
    suspend fun getPreviewOrNull(): GuildPreview? = kord.with(rest).getGuildPreviewOrNull(id)

    /**
     * Requests to get the amount of users that would be pruned in this guild.
     *
     * A user is pruned if they have not been seen within the given [days]
     * and don't have a [Role] assigned in this guild.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun getPruneCount(days: Int = 7): Int =
            kord.rest.guild.getGuildPruneCount(id.value, days).pruned

    /**
     * Requests to prune users in this guild.
     *
     * A user is pruned if they have not been seen within the given [days]
     * and don't have a [Role] assigned in this guild.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun prune(days: Int = 7): Int {
        return kord.rest.guild.beginGuildPrune(id.value, days, true).pruned!!
    }

    /**
     * Requests to get the vanity url of this guild, if present.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun getVanityUrl(): String? {
        val identifier = kord.rest.guild.getVanityInvite(id.value).code ?: return null
        return "https://discord.gg/$identifier"
    }

    /**
     * Returns a new [GuildBehavior] with the given [strategy].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>): GuildBehavior = GuildBehavior(id, kord, strategy)

    companion object {
        internal operator fun invoke(id: Snowflake, kord: Kord, strategy: EntitySupplyStrategy<*> = kord.resources.defaultStrategy) = object : GuildBehavior {
            override val id: Snowflake = id
            override val kord: Kord = kord
            override val supplier: EntitySupplier = strategy.supply(kord)

            override fun hashCode(): Int = Objects.hash(id)

            override fun equals(other: Any?): Boolean = when (other) {
                is GuildBehavior -> other.id == id
                is PartialGuild -> other.id == id
                else -> false
            }
        }
    }

}

/**
 * Requests to edit this guild.
 *
 * @return The edited [Guild].
 *
 * @throws [RestRequestException] if something went wrong during the request.
 */
@OptIn(ExperimentalContracts::class)
suspend inline fun GuildBehavior.edit(builder: GuildModifyBuilder.() -> Unit): Guild {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    val response = kord.rest.guild.modifyGuild(id.value, builder)
    val data = GuildData.from(response)

    return Guild(data, kord)
}

@OptIn(ExperimentalContracts::class)
suspend inline fun GuildBehavior.createEmoji(builder: EmojiCreateBuilder.() -> Unit) {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    kord.rest.emoji.createEmoji(guildId = id.value, builder = builder)
}

/**
 * Requests to create a new text channel.
 *
 * @return The created [TextChannel].
 *
 * @throws [RestRequestException] if something went wrong during the request.
 */
@OptIn(ExperimentalContracts::class)
suspend inline fun GuildBehavior.createTextChannel(builder: TextChannelCreateBuilder.() -> Unit): TextChannel {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    val response = kord.rest.guild.createTextChannel(id.value, builder)
    val data = ChannelData.from(response)

    return Channel.from(data, kord) as TextChannel
}

/**
 * Requests to create a new voice channel.
 *
 * @return The created [VoiceChannel].
 *
 * @throws [RestRequestException] if something went wrong during the request.
 */
@OptIn(ExperimentalContracts::class)
suspend inline fun GuildBehavior.createVoiceChannel(builder: VoiceChannelCreateBuilder.() -> Unit): VoiceChannel {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    val response = kord.rest.guild.createVoiceChannel(id.value, builder)
    val data = ChannelData.from(response)

    return Channel.from(data, kord) as VoiceChannel
}

/**
 * Requests to create a new news channel.
 *
 * @return The created [NewsChannel].
 *
 * @throws [RestRequestException] if something went wrong during the request.
 */
@OptIn(ExperimentalContracts::class)
@KordPreview
suspend inline fun GuildBehavior.createNewsChannel(builder: NewsChannelCreateBuilder.() -> Unit): NewsChannel {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    val response = kord.rest.guild.createNewsChannel(id.value, builder)
    val data = ChannelData.from(response)

    return Channel.from(data, kord) as NewsChannel
}

/**
 * Requests to create a new category.
 *
 * @return The created [Category].
 *
 * @throws [RestRequestException] if something went wrong during the request.
 */
@OptIn(ExperimentalContracts::class)
suspend inline fun GuildBehavior.createCategory(builder: CategoryCreateBuilder.() -> Unit): Category {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    val response = kord.rest.guild.createCategory(id.value, builder)
    val data = ChannelData.from(response)

    return Channel.from(data, kord) as Category
}

/**
 * Requests to swap positions of channels in this guild.
 *
 * @throws [RestRequestException] if something went wrong during the request.
 */
@OptIn(ExperimentalContracts::class)
suspend inline fun GuildBehavior.swapChannelPositions(builder: GuildChannelPositionModifyBuilder.() -> Unit) {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    kord.rest.guild.modifyGuildChannelPosition(id.value, builder)
}

/**
 * Requests to swap positions of roles in this guild.
 *
 * This request will execute regardless of the consumption of the return value.
 *
 * @return the roles of this guild after the update in an unspecified order, call [sorted] to get a consistent order.
 *
 * @throws [RestRequestException] if something went wrong during the request.
 */
@OptIn(ExperimentalContracts::class)
suspend inline fun GuildBehavior.swapRolePositions(builder: RolePositionsModifyBuilder.() -> Unit): Flow<Role> {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    val response = kord.rest.guild.modifyGuildRolePosition(id.value, builder)
    return response.asFlow().map { RoleData.from(id.value, it) }.map { Role(it, kord) }

}

/**
 * Requests to add a new role to this guild.
 *
 * @return The created [Role].
 *
 * @throws [RestRequestException] if something went wrong during the request.
 */
@OptIn(ExperimentalContracts::class)
suspend inline fun GuildBehavior.addRole(builder: RoleCreateBuilder.() -> Unit): Role {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    val response = kord.rest.guild.createGuildRole(id.value, builder)
    val data = RoleData.from(id.value, response)

    return Role(data, kord)
}

/**
 * Requests to ban the given [userId] in this guild.
 *
 * @throws [RestRequestException] if something went wrong during the request.
 */
@OptIn(ExperimentalContracts::class)
suspend inline fun GuildBehavior.ban(userId: Snowflake, builder: BanCreateBuilder.() -> Unit) {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    kord.rest.guild.addGuildBan(guildId = id.value, userId = userId.value, builder = builder)
}

/**
 * Requests to get the [GuildChannel] represented by the [channelId] as type [T].
 *
 * @throws [RequestException] if anything went wrong during the request.
 * @throws [EntityNotFoundException] if the [T] wasn't present.
 * @throws [ClassCastException] if the channel is not of type [T].
 * @throws [IllegalArgumentException] if the channel is not part of this guild.
 */
suspend inline fun<reified T: GuildChannel> GuildBehavior.getChannelOf(channelId: Snowflake) : T {
    val channel = supplier.getChannelOf<T>(channelId)
    require(channel.guildId == this.id) { "channel ${channelId.value} is not in guild ${this.id.value}" }
    return channel
}

/**
 * Requests to get the [GuildChannel] represented by the [channelId] as type [T],
 * returns null if the [GuildChannel] isn't present.
 *
 * @throws [RequestException] if anything went wrong during the request.
 * @throws [ClassCastException] if the channel is not of type [T].
 * @throws [IllegalArgumentException] if the channel is not part of this guild.
 */
suspend inline fun<reified T: GuildChannel> GuildBehavior.getChannelOfOrNull(channelId: Snowflake) : T? {
    val channel = supplier.getChannelOfOrNull<T>(channelId) ?: return null
    require(channel.guildId == this.id) { "channel ${channelId.value} is not in guild ${this.id.value}" }
    return channel
}

package com.gitlab.kordlib.core.behavior

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.cache.data.EmojiData
import com.gitlab.kordlib.core.entity.Entity
import com.gitlab.kordlib.core.entity.GuildEmoji
import com.gitlab.kordlib.core.entity.Strategizable
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.rest.builder.guild.EmojiModifyBuilder
import com.gitlab.kordlib.rest.request.RestRequestException
import java.util.*

/**
 * The behavior of a [Discord Emoij](https://discord.com/developers/docs/resources/emoji).
 */
interface GuildEmojiBehavior : Entity, Strategizable {

    /**
     * The id of the guild this emojis is part of.
     */
    val guildId: Snowflake

    /**
     * The behavior of the guild this emoji is part of.
     */
    val guild: GuildBehavior get() = GuildBehavior(guildId, kord)

    /**
     * Requests to delete this emoji.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun delete() {
        kord.rest.emoji.deleteEmoji(guildId = guildId.value, emojiId = id.value)
    }

    /**
     * Returns a new [GuildEmojiBehavior] with the given [strategy].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>): GuildEmojiBehavior =
            GuildEmojiBehavior(guildId = guildId, id = id, kord = kord, strategy = strategy)

    companion object {
        internal operator fun invoke(
                guildId: Snowflake,
                id: Snowflake,
                kord: Kord,
                strategy: EntitySupplyStrategy<*> = kord.resources.defaultStrategy
        ): GuildEmojiBehavior = object : GuildEmojiBehavior {
            override val guildId: Snowflake = guildId
            override val id: Snowflake = id
            override val kord: Kord = kord
            override val supplier: EntitySupplier = strategy.supply(kord)

            override fun hashCode(): Int = Objects.hash(id)

            override fun equals(other: Any?): Boolean = when(other) {
                is GuildEmojiBehavior -> other.id == id
                else -> false
            }
        }
    }
}

/**
 * Requests to edit this emoji.
 *
 * @return The edited [GuildEmoji].
 *
 * @throws [RestRequestException] if something went wrong during the request.
 */
suspend inline fun GuildEmojiBehavior.edit(builder: EmojiModifyBuilder.() -> Unit): GuildEmoji {
    val response = kord.rest.emoji.modifyEmoji(guildId.value, id.value, builder)
    val data = EmojiData.from(guildId = guildId.value, id = id.value, entity = response)

    return GuildEmoji(data, kord)
}

package com.gitlab.kordlib.core.behavior

import com.gitlab.kordlib.cache.api.query
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.cache.data.PresenceData
import com.gitlab.kordlib.core.cache.data.VoiceStateData
import com.gitlab.kordlib.core.entity.*
import com.gitlab.kordlib.core.exception.EntityNotFoundException
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.rest.builder.ban.BanCreateBuilder
import com.gitlab.kordlib.rest.builder.member.MemberModifyBuilder
import com.gitlab.kordlib.rest.request.RestRequestException
import java.util.*
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

/**
 * The behavior of a [Discord Member](https://discord.com/developers/docs/resources/guild#guild-member-object).
 */
interface MemberBehavior : Entity, UserBehavior {

    /**
     * The id of the guild this channel is associated to.
     */
    val guildId: Snowflake

    /**
     * The guild this channel is associated to.
     */
    val guild: GuildBehavior get() = GuildBehavior(guildId, kord)

    /**
     * The raw mention for this member's nickname.
     */
    val nicknameMention get() = "<@!${id.value}>"

    /**
     * Requests to get the this behavior as a [Member].
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the member wasn't present.
     */
    suspend fun asMember(): Member = supplier.getMember(guildId, id)

    /**
     * Requests to get this behavior as a [Member],
     * returns null if the member isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun asMemberOrNull(): Member? = supplier.getMemberOrNull(guildId, id)

    /**
     * Requests to unban this member from its guild.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun unban() = guild.unBan(id)

    /**
     * Requests to kick this member from its guild.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun kick(reason: String? = null) = guild.kick(id, reason)

    /**
     * Requests to add the [Role] with the [roleId] to this member.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    @Deprecated("use the overload with reason instead", level = DeprecationLevel.HIDDEN)
    suspend fun addRole(roleId: Snowflake) {//TODO remove in Kord 0.7.0
        kord.rest.guild.addRoleToGuildMember(guildId = guildId.value, userId = id.value, roleId = roleId.value)
    }

    /**
     * Requests to add the [Role] with the [roleId] to this member.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun addRole(roleId: Snowflake, reason: String? = null) {
        kord.rest.guild.addRoleToGuildMember(guildId = guildId.value, userId = id.value, roleId = roleId.value, reason = reason)
    }

    /**
     * Requests to get the [Guild] this member is part of.
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the [Guild] wasn't present.
     */
    suspend fun getGuild(): Guild = supplier.getGuild(guildId)

    /**
     * Requests to get the [Guild] this member is part of,
     * returns null if the [Guild] isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getGuildOrNull(): Guild? = supplier.getGuildOrNull(guildId)

    /**
     * Requests to remove the [Role] with the [roleId] from this member.
     *
     * @throws [RequestException] if something went wrong during the request.
     */
    @Deprecated("use the overload with reason instead", level = DeprecationLevel.HIDDEN)
    suspend fun removeRole(roleId: Snowflake) {//TODO remove in Kord 0.7.0
        kord.rest.guild.deleteRoleFromGuildMember(guildId = guildId.value, userId = id.value, roleId = roleId.value)
    }

    /**
     * Requests to remove the [Role] with the [roleId] from this member.
     *
     * @throws [RequestException] if something went wrong during the request.
     */
    suspend fun removeRole(roleId: Snowflake, reason: String? = null) {
        kord.rest.guild.deleteRoleFromGuildMember(guildId = guildId.value, userId = id.value, roleId = roleId.value, reason = reason)
    }

    /**
     * Requests to get the [Presence] of this member in the [guild].
     *
     * This property is not resolvable through REST and will always use the [KordCache] instead.
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the [Presence] wasn't present.
     */
    suspend fun getPresence(): Presence = getPresenceOrNull() ?: EntityNotFoundException.guildEntityNotFound(
            "Presence for Member",
            guildId = guildId,
            id = id
    )

    /**
     * Requests to get the [Presence] of this member in the [guild],
     * returns null if the [Presence] isn't present.
     *
     * This property is not resolvable through REST and will always use the [KordCache] instead.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getPresenceOrNull(): Presence? {
        val data = kord.cache.query<PresenceData> {
            PresenceData::userId eq id.longValue
            PresenceData::guildId eq guildId.longValue
        }.singleOrNull() ?: return null

        return Presence(data, kord)
    }

    /**
     * Requests to get the [VoiceState] of this member in the [guild].
     *
     * This property is not resolvable through REST and will always use the [KordCache] instead.
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the [VoiceState] wasn't present.
     */
    suspend fun getVoiceState(): VoiceState = getVoiceStateOrNull() ?: EntityNotFoundException.guildEntityNotFound(
            "VoiceState for Member",
            guildId = guildId,
            id = id
    )

    /**
     * Requests to get the [VoiceState] of this member in the [guild],
     * returns null if the [VoiceState] isn't present.
     *
     * This property is not resolvable through REST and will always use the [KordCache] instead.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getVoiceStateOrNull(): VoiceState? {
        val data = kord.cache.query<VoiceStateData> {
            VoiceStateData::userId eq id.longValue
            VoiceStateData::guildId eq guildId.longValue
        }.singleOrNull() ?: return null

        return VoiceState(data, kord)
    }

    /**
     * Returns a new [MemberBehavior] with the given [strategy].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>): MemberBehavior = MemberBehavior(guildId = guildId, id = id, kord = kord, strategy = strategy)

    companion object {
        internal operator fun invoke(guildId: Snowflake, id: Snowflake, kord: Kord, strategy: EntitySupplyStrategy<*> = kord.resources.defaultStrategy): MemberBehavior = object : MemberBehavior {
            override val guildId: Snowflake = guildId
            override val id: Snowflake = id
            override val kord: Kord = kord
            override val supplier: EntitySupplier = strategy.supply(kord)

            override fun hashCode(): Int = Objects.hash(id, guildId)

            override fun equals(other: Any?): Boolean = when(other) {
                is MemberBehavior -> other.id == id && other.guildId == guildId
                is UserBehavior -> other.id == id
                else -> false
            }
        }
    }

}

/**
 * Requests to ban this member.
 *
 * @throws [RestRequestException] if something went wrong during the request.
 */
@OptIn(ExperimentalContracts::class)
suspend inline fun MemberBehavior.ban(builder: BanCreateBuilder.() -> Unit = {}) {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    guild.ban(id, builder)
}

/**
 * Requests to edit this member.
 *
 * @throws [RestRequestException] if something went wrong during the request.
 */
@OptIn(ExperimentalContracts::class)
suspend inline fun MemberBehavior.edit(builder: MemberModifyBuilder.() -> Unit) {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    kord.rest.guild.modifyGuildMember(guildId.value, id.value, builder)
}

package com.gitlab.kordlib.core.behavior

import com.gitlab.kordlib.common.annotation.KordPreview
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.channel.MessageChannelBehavior
import com.gitlab.kordlib.core.cache.data.MessageData
import com.gitlab.kordlib.core.cache.data.UserData
import com.gitlab.kordlib.core.entity.*
import com.gitlab.kordlib.core.exception.EntityNotFoundException
import com.gitlab.kordlib.core.paginateForwards
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.rest.builder.message.MessageModifyBuilder
import com.gitlab.kordlib.rest.service.RestClient
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import java.util.*
import com.gitlab.kordlib.rest.request.RestRequestException
import com.gitlab.kordlib.common.entity.Permission
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

/**
 * The behavior of a [Discord Message](https://discord.com/developers/docs/resources/channel#message-object).
 */
interface MessageBehavior : Entity, Strategizable {
    /**
     * The channel id this message belongs to.
     */
    val channelId: Snowflake

    /**
     * The channel behavior that this message belongs to.
     */
    val channel get() = MessageChannelBehavior(channelId, kord)

    /**
     * Requests to get the this behavior as a [Message].
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the message wasn't present.
     */
    suspend fun asMessage(): Message = supplier.getMessage(channelId = channelId, messageId = id)

    /**
     * Requests to get this behavior as a [Message],
     * returns null if the message isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun asMessageOrNull(): Message? = supplier.getMessageOrNull(channelId = channelId, messageId = id)

    /**
     * Requests to delete this message.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun delete() {
        kord.rest.channel.deleteMessage(channelId = channelId.value, messageId = id.value)
    }

    /**
     * Requests to get all users that have reacted to this message.
     *
     * This property is not resolvable through cache and will always use the [RestClient] instead.
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */

    fun getReactors(emoji: ReactionEmoji): Flow<User> =
            kord.with(EntitySupplyStrategy.rest).getReactors(channelId, id, emoji)

    /**
     * Requests to add an [emoji] to this message.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun addReaction(emoji: ReactionEmoji) {
        kord.rest.channel.createReaction(channelId = channelId.value, messageId = id.value, emoji = emoji.urlFormat)
    }

    /**
     * Requests to add an [emoji] to this message.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun addReaction(emoji: GuildEmoji) {
        addReaction(ReactionEmoji.from(emoji))
    }

    /**
     * Requests to publish this message to following channels, this function assumes the message was created
     * in an announcement channel.
     *
     * Requires the [Permission.SendMessages] permission if the bot created the message,
     * or the [Permission.ManageChannels] permission otherwise.
     *
     * @return The updated message after publishing.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    @KordPreview
    suspend fun publish() : Message {
        val response = kord.rest.channel.crossPost(channelId =  channelId.value, messageId = id.value)
        val data = MessageData.from(response)
        return Message(data, kord)
    }

    /**
     * Requests to delete an [emoji] from this message made by a [userId].
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun deleteReaction(userId: Snowflake, emoji: ReactionEmoji) {
        kord.rest.channel.deleteReaction(channelId = channelId.value, messageId = id.value, userId = userId.value, emoji = emoji.urlFormat)
    }

    /**
     * Requests to delete an [emoji] from this message made by this bot.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun deleteOwnReaction(emoji: ReactionEmoji) {
        kord.rest.channel.deleteOwnReaction(channelId = channelId.value, messageId = id.value, emoji = emoji.urlFormat)
    }

    /**
     * Requests to delete all reactions from this message.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun deleteAllReactions() {
        kord.rest.channel.deleteAllReactions(channelId = channelId.value, messageId = id.value)
    }

    /**
     * Requests to delete all [emoji] reactions from this message.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun deleteReaction(emoji: ReactionEmoji) {
        kord.rest.channel.deleteAllReactionsForEmoji(channelId = channelId.value, messageId = id.value, emoji = emoji.urlFormat)
    }

    /**
     * Requests to pin this message.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun pin() {
        kord.rest.channel.addPinnedMessage(channelId = channelId.value, messageId = id.value)
    }

    /**
     * Requests to unpin this message.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun unpin() {
        kord.rest.channel.deletePinnedMessage(channelId = channelId.value, messageId = id.value)
    }

    /**
     * Returns a new [MessageBehavior] with the given [strategy].
     */
    override fun withStrategy(
            strategy: EntitySupplyStrategy<*>
    ): MessageBehavior = MessageBehavior(channelId, id, kord, strategy)

    companion object {
        internal operator fun invoke(
                channelId: Snowflake,
                messageId: Snowflake,
                kord: Kord, strategy:
                EntitySupplyStrategy<*> = kord.resources.defaultStrategy
        ) = object : MessageBehavior {
            override val channelId: Snowflake = channelId
            override val id: Snowflake = messageId
            override val kord: Kord = kord
            override val supplier: EntitySupplier = strategy.supply(kord)

            override fun hashCode(): Int = Objects.hash(id)

            override fun equals(other: Any?): Boolean = when (other) {
                is MessageBehavior -> other.id == id && other.channelId == channelId
                else -> false
            }
        }
    }

}

/**
 * Requests to edit this message.
 *
 * @return The edited [Message].
 *
 * @throws [RestRequestException] if something went wrong during the request.
 */
@OptIn(ExperimentalContracts::class)
suspend inline fun MessageBehavior.edit(builder: MessageModifyBuilder.() -> Unit): Message {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    val response = kord.rest.channel.editMessage(channelId = channelId.value, messageId = id.value, builder = builder)
    val data = MessageData.from(response)

    return Message(data, kord)
}

package com.gitlab.kordlib.core.behavior

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.cache.data.RoleData
import com.gitlab.kordlib.core.entity.Entity
import com.gitlab.kordlib.core.entity.Role
import com.gitlab.kordlib.core.entity.Strategizable
import com.gitlab.kordlib.core.indexOfFirstOrNull
import com.gitlab.kordlib.core.sorted
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.rest.builder.role.RoleModifyBuilder
import com.gitlab.kordlib.rest.request.RestRequestException
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.map
import java.util.*
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

/**
 * The behavior of a [Discord Role](https://discord.com/developers/docs/topics/permissions#role-object) associated to a [guild].
 */
interface RoleBehavior : Entity, Strategizable {
    /**
     * The id of the guild this channel is associated to.
     */
    val guildId: Snowflake

    /**
     * The guild behavior this channel is associated to.
     */
    val guild: GuildBehavior get() = GuildBehavior(guildId, kord)

    /**
     * The raw mention of this entity.
     */
    val mention: String get() = "<@&${id.value}>"

    /**
     * Requests to change the [position] of this role.
     *
     * This request will execute regardless of the consumption of the return value.
     *
     * @return The roles in of this [guild] in updated order.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun changePosition(position: Int): Flow<Role> {
        val response = kord.rest.guild.modifyGuildRolePosition(guildId.value) {
            move(id to position)
        }
        return response.asFlow().map { RoleData.from(guildId.value, it) }.map { Role(it, kord) }.sorted()
    }

    /**
     * Requests to get the position of this role in the role list of this guild.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun getPosition(): Int = supplier.getGuildRoles(guildId).sorted().indexOfFirstOrNull { it.id == id }!!

    /**
     * Requests to delete this role.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun delete() {
        kord.rest.guild.deleteGuildRole(guildId = guildId.value, roleId = id.value)
    }

    /**
     * Returns a new [RoleBehavior] with the given [strategy].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>): RoleBehavior = RoleBehavior(guildId, id, kord, strategy)

    companion object {
        internal operator fun invoke(guildId: Snowflake, id: Snowflake, kord: Kord, strategy: EntitySupplyStrategy<*> = kord.resources.defaultStrategy): RoleBehavior = object : RoleBehavior {
            override val guildId: Snowflake = guildId
            override val id: Snowflake = id
            override val kord: Kord = kord
            override val supplier: EntitySupplier = strategy.supply(kord)

            override fun hashCode(): Int = Objects.hash(id, guildId)

            override fun equals(other: Any?): Boolean = when(other) {
                is RoleBehavior -> other.id == id && other.guildId == guildId
                else -> false
            }
        }
    }
}

/**
 * Requests to edit this role.
 *
 * @return The edited [Role].
 *
 * @throws [RestRequestException] if something went wrong during the request.
 */
@OptIn(ExperimentalContracts::class)
suspend inline fun RoleBehavior.edit(builder: RoleModifyBuilder.() -> Unit): Role {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    val response = kord.rest.guild.modifyGuildRole(guildId = guildId.value, roleId = id.value, builder = builder)
    val data = RoleData.from(id.value, response)

    return Role(data, kord)
}

package com.gitlab.kordlib.core.behavior

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.cache.data.ChannelData
import com.gitlab.kordlib.core.entity.*
import com.gitlab.kordlib.core.entity.channel.Channel
import com.gitlab.kordlib.core.entity.channel.DmChannel
import com.gitlab.kordlib.core.exception.EntityNotFoundException
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.rest.json.request.DMCreateRequest
import com.gitlab.kordlib.rest.request.RestRequestException
import com.gitlab.kordlib.rest.service.RestClient
import io.ktor.http.HttpStatusCode
import java.util.*

/**
 * The behavior of a [Discord User](https://discord.com/developers/docs/resources/user)
 */
interface UserBehavior : Entity, Strategizable {

    val mention: String get() = "<@${id.value}>"

    /**
     * Requests to get the this behavior as a [Member] in the [Guild] with the [guildId].
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the member wasn't present.
     */
    suspend fun asMember(guildId: Snowflake): Member = supplier.getMember(guildId = guildId, userId = id)

    /**
     * Requests to get this behavior as a [Member] in the [Guild] with the [guildId],
     * returns null if the member isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun asMemberOrNull(guildId: Snowflake): Member? = supplier.getMemberOrNull(guildId = guildId, userId = id)

    /**
     * Requests to get the this behavior as a [User].
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the user wasn't present.
     */
    suspend fun asUser(): User = supplier.getUser(id)

    /**
     * Requests to get this behavior as a [User],
     * returns null if the user isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun asUserOrNull(): User? = supplier.getUserOrNull(id)

    /**
     * Requests to get or create a [DmChannel] between this bot and the user.
     *
     * This property is not resolvable through cache and will always use the [RestClient] instead.
     *
     * If a user does not allow you to send DM's to them, this method will throw a [RestRequestException] with
     * [code][RestRequestException.code] 403. This can be used to handle the edge case accordingly:
     * ```kotlin
     * val channel = try {
     *     user.getDmChannel()
     * } catch (exception: RestRequestException) {
     *     if(exception.code == HttpStatusCode.Forbidden.value) {
     *         //user doesn't have DMs enabled
     *         TODO("handle edge case")
     *     } else throw exception
     * }
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun getDmChannel(): DmChannel {
        val response = kord.rest.user.createDM(DMCreateRequest(id.value))
        val data = ChannelData.from(response)

        return Channel.from(data, kord) as DmChannel
    }

    /**
     * returns a new [UserBehavior] with the given [strategy].
     *
     * @param strategy the strategy to use for the new instance. By default [EntitySupplyStrategy.CacheWithRestFallback].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>): UserBehavior = UserBehavior(id, kord, strategy)

    companion object {
        internal operator fun invoke(id: Snowflake, kord: Kord, strategy: EntitySupplyStrategy<*> = kord.resources.defaultStrategy): UserBehavior = object : UserBehavior {
            override val id: Snowflake = id
            override val kord: Kord = kord
            override val supplier: EntitySupplier = strategy.supply(kord)

            override fun hashCode(): Int = Objects.hash(id)

            override fun equals(other: Any?): Boolean = when(other) {
                is UserBehavior -> other.id == id
                else -> false
            }
        }
    }

}

package com.gitlab.kordlib.core.behavior

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.cache.data.MessageData
import com.gitlab.kordlib.core.cache.data.WebhookData
import com.gitlab.kordlib.core.entity.Entity
import com.gitlab.kordlib.core.entity.Message
import com.gitlab.kordlib.core.entity.Strategizable
import com.gitlab.kordlib.core.entity.Webhook
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.rest.builder.webhook.ExecuteWebhookBuilder
import com.gitlab.kordlib.rest.builder.webhook.WebhookModifyBuilder
import com.gitlab.kordlib.rest.request.RestRequestException
import java.util.*
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

/**
 * The behavior of a [Discord Webhook](https://discord.com/developers/docs/resources/webhook).
 */
interface WebhookBehavior : Entity, Strategizable {

    /**
     * Requests to delete this webhook, this user must be the creator.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun delete(reason: String? = null) {
        kord.rest.webhook.deleteWebhook(id.value, reason)
    }

    /**
     * Requests to delete this webhook.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun delete(token: String, reason: String? = null) {
        kord.rest.webhook.deleteWebhookWithToken(id.value, token, reason)
    }

    /**
     * Returns a new [WebhookBehavior] with the given [strategy].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>): WebhookBehavior =
            WebhookBehavior(id, kord, strategy)

    companion object {
        internal operator fun invoke(
                id: Snowflake,
                kord: Kord,
                strategy: EntitySupplyStrategy<*> = kord.resources.defaultStrategy
        ): WebhookBehavior = object : WebhookBehavior {
            override val id: Snowflake = id
            override val kord: Kord = kord
            override val supplier: EntitySupplier = strategy.supply(kord)

            override fun hashCode(): Int = Objects.hash(id)

            override fun equals(other: Any?): Boolean = when (other) {
                is WebhookBehavior -> other.id == id
                else -> false
            }
        }
    }

}

/**
 * Requests to edit the webhook, this user must be the creator.
 *
 * @return The updated [Webhook].
 *
 * @throws [RestRequestException] if something went wrong during the request.
 */
@OptIn(ExperimentalContracts::class)
suspend inline fun WebhookBehavior.edit(builder: WebhookModifyBuilder.() -> Unit): Webhook {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    val response = kord.rest.webhook.modifyWebhook(id.value, builder)
    val data = WebhookData.from(response)

    return Webhook(data, kord)
}

/**
 * Requests to edit the webhook.
 *
 * @return The updated [Webhook].
 *
 * @throws [RestRequestException] if something went wrong during the request.
 */
@OptIn(ExperimentalContracts::class)
suspend inline fun WebhookBehavior.edit(token: String, builder: WebhookModifyBuilder.() -> Unit): Webhook {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    val response = kord.rest.webhook.modifyWebhookWithToken(id.value, token, builder)
    val data = WebhookData.from(response)

    return Webhook(data, kord)
}

/**
 * Requests to execute this webhook.
 *
 * @throws [RestRequestException] if something went wrong during the request.
 */
@OptIn(ExperimentalContracts::class)
suspend inline fun WebhookBehavior.execute(token: String, builder: ExecuteWebhookBuilder.() -> Unit): Message {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    val response = kord.rest.webhook.executeWebhook(
            token = token,
            webhookId = id.value,
            wait = true,
            builder = builder
    )!!

    val data = MessageData.from(response)

    return Message(data, kord)
}

/**
 * Requests to execute this webhook.
 *
 * This is a 'fire and forget' variant of [execute]. It will not wait for a response and might not throw an
 * Exception if the request wasn't executed.
 */
@OptIn(ExperimentalContracts::class)
suspend inline fun WebhookBehavior.executeIgnored(token: String, builder: ExecuteWebhookBuilder.() -> Unit) {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    kord.rest.webhook.executeWebhook(token = token, webhookId = id.value, wait = false, builder = builder)
}

package com.gitlab.kordlib.core.cache

import com.gitlab.kordlib.cache.api.DataCache
import com.gitlab.kordlib.gateway.Close
import com.gitlab.kordlib.gateway.Gateway
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlin.coroutines.CoroutineContext

/**
 * A bridge between [DataCache] and [Gateway] that automatically empties cache on disconnect.
 */
@FlowPreview
class CachingGateway(
        private val cache: DataCache,
        private val gateway: Gateway,
        private val dispatcher: CoroutineDispatcher = Dispatchers.Default
) : DataCache by cache, Gateway by gateway, CoroutineScope {

    override val coroutineContext: CoroutineContext
        get() = Job() + dispatcher

    init {
        gateway.events.filterIsInstance<Close>().onEach { removeKordData() }.launchIn(this)
    }
}

package com.gitlab.kordlib.core.cache

import com.gitlab.kordlib.cache.api.DataCache
import com.gitlab.kordlib.cache.api.find
import com.gitlab.kordlib.cache.api.query
import com.gitlab.kordlib.core.cache.data.*

/**
 * Registers all Kord data classes for this cache
 */
internal suspend fun DataCache.registerKordData() = register(
        RoleData.description,
        ChannelData.description,
        GuildData.description,
        MemberData.description,
        UserData.description,
        MessageData.description,
        EmojiData.description,
        WebhookData.description,
        PresenceData.description,
        VoiceStateData.description
)

/**
 * Removes all cached Kord data instances from this cache
 */
internal suspend fun DataCache.removeKordData() {
    query<RoleData>().remove()
    query<ChannelData>().remove()
    query<GuildData>().remove()
    query<MemberData>().remove()
    query<UserData>().remove()
    query<MessageData>().remove()
    query<EmojiData>().remove()
    query<WebhookData>().remove()
    query<PresenceData>().remove()
    query<VoiceStateData>().remove()
}

/**
 * Creates a [DataCacheView] for this view, only removing elements that were added
 * directly to this instance.
 */
suspend fun DataCache.createView(): DataCacheView = DataCacheView(this)

package com.gitlab.kordlib.core.cache

import com.gitlab.kordlib.cache.api.DataCache
import com.gitlab.kordlib.cache.api.DataEntryCache
import com.gitlab.kordlib.cache.api.Query
import com.gitlab.kordlib.cache.api.QueryBuilder
import com.gitlab.kordlib.cache.api.data.DataDescription
import kotlin.reflect.KProperty1
import kotlin.reflect.KType

class ViewKeys(private val keySet: MutableSet<Any> = mutableSetOf()) {
    val keys: Set<Any> = keySet

    fun add(key: Any) {
        keySet.add(key)
    }
}

/**
 * A [DataCacheView] that limits removal of cached instances to those inserted in this cache,
 * and not the underlying [cache].
 */
class DataCacheView(private val cache: DataCache) : DataCache by cache {
    private val keys = ViewKeys()
    private val descriptions = mutableMapOf<KType, DataDescription<out Any, out Any>>()

    @Suppress("UNCHECKED_CAST")
    private fun <T : Any> getDescription(type: KType) = descriptions[type]!! as DataDescription<T, out Any>

    override suspend fun register(description: DataDescription<out Any, out Any>) {
        descriptions[description.type] = description
    }

    override suspend fun register(vararg descriptions: DataDescription<out Any, out Any>) {
        descriptions.forEach { register(it) }
    }

    override suspend fun register(descriptions: Iterable<DataDescription<out Any, out Any>>) {
        descriptions.forEach { register(it) }
    }

    override fun <T : Any> getEntry(type: KType): DataEntryCache<T>? {
        return cache.getEntry<T>(type)?.let { DataEntryCacheView(it, getDescription(type), keys) }
    }

}

private class DataEntryCacheView<T : Any>(
        private val entryCache: DataEntryCache<T>,
        private val description: DataDescription<T, out Any>,
        private val viewKeys: ViewKeys
) : DataEntryCache<T> by entryCache {

    override suspend fun put(item: T) {
        entryCache.put(item)
        viewKeys.add(description.indexField.property.get(item))
    }

    override fun query(): QueryBuilder<T> {
        return QueryBuilderView(entryCache.query(), description.indexField.property, viewKeys.keys)
    }

}

private class QueryBuilderView<T : Any>(
        private val builder: QueryBuilder<T>,
        private val property: KProperty1<T, Any>,
        private val keys: Set<Any>
) : QueryBuilder<T> by builder {
    override fun build(): Query<T> = QueryView(builder, property, keys)
}

private class QueryView<T : Any>(
        private val builder: QueryBuilder<T>,
        private val property: KProperty1<T, Any>,
        private val keys: Set<Any>
) : Query<T> by builder.build() {
    override suspend fun remove() = builder.apply { property `in` keys }.build().remove()
}

package com.gitlab.kordlib.core.cache

import com.gitlab.kordlib.cache.api.DataCache
import com.gitlab.kordlib.cache.api.DataEntryCache
import com.gitlab.kordlib.cache.api.data.DataDescription
import com.gitlab.kordlib.cache.api.delegate.DelegatingDataCache
import com.gitlab.kordlib.cache.api.delegate.EntrySupplier
import com.gitlab.kordlib.cache.map.MapLikeCollection
import com.gitlab.kordlib.cache.map.internal.MapEntryCache
import com.gitlab.kordlib.cache.map.lruLinkedHashMap
import com.gitlab.kordlib.core.cache.data.*
import java.util.concurrent.ConcurrentHashMap

typealias Generator<I, T> = (cache: DataCache, description: DataDescription<T, I>) -> DataEntryCache<out T>

class KordCacheBuilder {

    /**
     * The default behavior for all types not explicitly configured, by default a [ConcurrentHashMap] is supplied.
     */
    var defaultGenerator: Generator<Any, Any> = { cache, description ->
        MapEntryCache(cache, description, MapLikeCollection.concurrentHashMap())
    }

    private val descriptionGenerators: MutableMap<DataDescription<*, *>, Generator<*, *>> = mutableMapOf()

    init {
        messages { _, _ -> DataEntryCache.none() }
    }

    /**
     * Disables caching for all entries, clearing all custom generators and setting the default to [none].
     */
    fun disableAll() {
        descriptionGenerators.clear()
        defaultGenerator = none()
    }

    /**
     * A Generator creating [DataEntryCaches][DataEntryCache] that won't store any entities, can be used to disable caching.
     */
    fun <T : Any, I : Any> none(): Generator<T, I> = { _, _ -> DataEntryCache.none() }

    /**
     * A Generator creating [DataEntryCaches][DataEntryCache] with a maximum [size], removing items on last insertion.
     * Shortcut for [lruLinkedHashMap].
     */
    fun <T : Any, I : Any> lruCache(size: Int = 100): Generator<T, I> = { cache, description ->
        MapEntryCache(cache, description, MapLikeCollection.lruLinkedHashMap(size))
    }

    @Suppress("UNCHECKED_CAST")
    fun <T : Any, I : Any> forDescription(description: DataDescription<T, I>, generator: Generator<T, I>?) {
        if (generator == null) return run {
            descriptionGenerators.remove(description)
        }
        descriptionGenerators[description] = generator as Generator<*, *>
    }

    /**
     *  Configures the caching for [MessageData].
     *  Your application will generally handle a lot of messages during its lifetime, as such it's advised to
     *  limit the amount of messages cached in some way.
     *
     *  ```kotlin
     *  cache {
     *      messages(none()) //disable caching entirely
     *      messages(lruCache(100)) //only keep the latest 100 messages
     *  }
     *  ```
     */
    fun messages(generator: Generator<MessageData, Long>) = forDescription(MessageData.description, generator)

    /**
     *  Configures the caching for [RoleData].
     */
    fun roles(generator: Generator<RoleData, Long>) = forDescription(RoleData.description, generator)

    /**
     *  Configures the caching for [ChannelData].
     */
    fun channels(generator: Generator<ChannelData, Long>) = forDescription(ChannelData.description, generator)

    /**
     *  Configures the caching for [GuildData].
     */
    fun guilds(generator: Generator<GuildData, Long>) = forDescription(GuildData.description, generator)

    /**
     *  Configures the caching for [MemberData].
     *  It's advised to configure user and member data similarly, so that every member in cache also has its user data cached.
     *  Failing to do so would result in a performance hit when fetching members.
     */
    fun members(generator: Generator<MemberData, String>) = forDescription(MemberData.description, generator)

    /**
     *  Configures the caching for [UserData].
     *  It's advised to configure user and member data similarly, so that every member in cache also has its user data cached.
     *  Failing to do so would result in a performance hit when fetching members.
     */
    fun users(generator: Generator<UserData, Long>) = forDescription(UserData.description, generator)

    /**
     *  Configures the caching for [EmojiData].
     */
    fun emojis(generator: Generator<EmojiData, Long>) = forDescription(EmojiData.description, generator)

    /**
     *  Configures the caching for [WebhookData].
     */
    fun webhooks(generator: Generator<WebhookData, Long>) = forDescription(WebhookData.description, generator)

    /**
     *  Configures the caching for [PresenceData].
     */
    fun presences(generator: Generator<PresenceData, String>) = forDescription(PresenceData.description, generator)

    /**
     *  Configures the caching for [VoiceStateData].
     */
    fun voiceState(generator: Generator<VoiceStateData, String>) = forDescription(VoiceStateData.description, generator)

    fun build(): DataCache = DelegatingDataCache(EntrySupplier.invoke { cache, description ->
        val generator = descriptionGenerators[description] ?: defaultGenerator
        generator(cache, description)
    })

}

package com.gitlab.kordlib.core.entity

import com.gitlab.kordlib.common.entity.ActivityType
import com.gitlab.kordlib.common.entity.DiscordPartialEmoji
import com.gitlab.kordlib.core.cache.data.ActivityData
import com.gitlab.kordlib.core.toInstant
import java.time.Instant

class Activity(val data: ActivityData) {

    val name: String get() = data.name
    val type: ActivityType get() = data.type
    val url: String? get() = data.url
    val start: Instant? get() = data.start?.toInstant()
    val stop: Instant? get() = data.stop?.toInstant()
    val applicationId: String? get() = data.applicationId
    val details: String? get() = data.details
    val emoji: DiscordPartialEmoji? get() = data.emoji
    val state: String? get() = data.state
    val party: Party? get() = data.partyId?.let { Party(it, data.partyCurrentSize!!, data.partyMaxSize!!) }
    val assets: Assets? get() = Assets(data.largeImage, data.largeText, data.smallImage, data.smallText)
    val secrets: Secrets? get() = Secrets(data.secretsJoin, data.secretsSpectate, data.secretsMatch)
    val isInstance: Boolean? get() = data.instance
    val flags: ActivityFlags
        get() = ActivityFlags(data.flags ?: 0)

    internal fun toGatewayActivity() =
            com.gitlab.kordlib.common.entity.DiscordActivity(name, type, url)

    data class Party(val id: String, val currentSize: Int, val maxSize: Int)
    data class Assets(val largeImage: String?, val largeText: String?, val smallImage: String?, val smallText: String?)
    data class Secrets(val join: String?, val spectate: String?, val match: String?)

}

inline class ActivityFlags(val code: Int) {
    operator fun contains(flag: ActivityFlag) = code.shr(flag.position).and(1) == 1
}

enum class ActivityFlag(val position: Int) {
    Instance(0),
    Join(1),
    Spectate(2),
    JoinRequest(3),
    Sync(4),
    Play(5)
}

package com.gitlab.kordlib.core.entity

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.UserBehavior
import com.gitlab.kordlib.core.cache.data.ApplicationInfoData
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.rest.Image
import java.util.*

/**
 * The details of a [Discord OAuth2](https://discord.com/developers/docs/topics/oauth2) application.
 */
class ApplicationInfo(
        val data: ApplicationInfoData,
        override val kord: Kord,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : Entity, Strategizable {

    override val id: Snowflake
        get() = Snowflake(data.id)

    val name: String get() = data.name

    val description: String? get() = data.description

    val isPublic: Boolean get() = data.botPublic

    val requireCodeGrant: Boolean get() = data.botRequireCodeGrant

    val ownerId: Snowflake get() = Snowflake(data.ownerId)

    val owner: UserBehavior get() = UserBehavior(ownerId, kord)

    val summary: String get() = data.summary

    val verifyKey: String get() = data.verifyKey

    val teamId: Long? get() = data.teamId

    val guildId: Long? get() = data.guildId

    val primarySkuId: Long? get() = data.primarySkuId

    val slug: String? get() = data.slug

    val coverImage: String? get() = data.coverImage

    suspend fun getOwner(): User = supplier.getUser(ownerId)

    suspend fun getOwnerOrNull(): User? = supplier.getUserOrNull(ownerId)

    suspend fun getCoverImage(): Image? = coverImage?.let { Image.fromUrl(kord.resources.httpClient, it) }

    /**
     * Returns a new [ApplicationInfo] with the given [strategy].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>): ApplicationInfo =
            ApplicationInfo(data, kord, strategy.supply(kord))

    override fun hashCode(): Int = Objects.hash(id)

    override fun equals(other: Any?): Boolean = when (other) {
        is ApplicationInfo -> other.id == id
        else -> false
    }
}

package com.gitlab.kordlib.core.entity

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.cache.data.AttachmentData
import java.util.*

/**
 * An instance of a [Discord Attachment](https://discord.com/developers/docs/resources/channel#attachment-object).
 *
 * A file attached to a [Message].
 */
data class Attachment(val data: AttachmentData, override val kord: Kord) : Entity {

    override val id: Snowflake
        get() = Snowflake(data.id)

    /**
     * The name of the file.
     */
    val filename: String get() = data.filename

    /**
     * The size of the file in bytes.
     */
    val size: Int get() = data.size

    /**
     * The url of the file.
     */
    val url: String get() = data.url

    /**
     * The proxied url of the file.
     */
    val proxyUrl: String get() = data.proxyUrl

    /**
     * The height of the file, if it is an image.
     */
    val height: Int? get() = data.height

    /**
     * The width of the file, if it is an image.
     */
    val width: Int? get() = data.width

    /**
     * If this file is displayed as a spoiler. Denoted by the `SPOILER_` prefix in the name.
     */
    val isSpoiler: Boolean get() = filename.startsWith("SPOILER_")

    /**
     * If this file is an image. Denoted by the presence of a [width] and [height].
     */
    val isImage: Boolean get() = height == width && height == null

    override fun hashCode(): Int = Objects.hash(id)

    override fun equals(other: Any?): Boolean = when(other) {
        is Attachment -> other.id == id
        else -> false
    }

}

package com.gitlab.kordlib.core.entity

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.KordObject
import com.gitlab.kordlib.core.behavior.UserBehavior
import com.gitlab.kordlib.core.cache.data.BanData
import com.gitlab.kordlib.core.exception.EntityNotFoundException
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy

/**
 * An instance of a [Discord Ban](https://discord.com/developers/docs/resources/guild#ban-object).
 */
class Ban(
        val data: BanData,
        override val kord: Kord,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : KordObject, Strategizable {

    /**
     * The id of the banned user.
     */
    val userId: Snowflake get() = Snowflake(data.userId)

    /**
     * The reason for the ban, if present.
     */
    val reason: String? get() = data.reason

    /**
     * The behavior of the banned user.
     */
    val user: UserBehavior get() = UserBehavior(id = userId, kord = kord)

    /**
     * Requests to get the [User] that was banned.
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the [User] wasn't present.
     */
    suspend fun getUser(): User = supplier.getUser(userId)

    /**
     * Requests to get the [User] that was banned,
     * returns null if the [User] isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getUserOrNull(): User? = supplier.getUserOrNull(userId)

    /**
     * Returns a new [Ban] with the given [strategy].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>) : Ban = Ban(data, kord, strategy.supply(kord))

}

package com.gitlab.kordlib.core.entity

import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.KordObject
import com.gitlab.kordlib.core.cache.data.*
import com.gitlab.kordlib.core.toInstant
import com.gitlab.kordlib.rest.builder.message.EmbedBuilder
import java.awt.Color
import java.time.Instant

internal const val embedDeprecationMessage = """
Embed types should be considered deprecated and might be removed in a future API version.

https://discord.com/developers/docs/resources/channel#embed-object-embed-types
"""

/**
 * An instance of a [Discord Embed](https://discord.com/developers/docs/resources/channel#embed-object).
 */
data class Embed(val data: EmbedData, override val kord: Kord) : KordObject {

    /**
     * The title, if present.
     */
    val title: String? get() = data.title

    /*
     * The type, [Embed.Type.Rich] for webhook and bot created embeds. Null if unknown.
     */
    @Suppress("DeprecatedCallableAddReplaceWith")
    @Deprecated(embedDeprecationMessage)
    val type: Type? get() = Type.values().firstOrNull { it.value == data.type }

    /**
     * The description, if present.
     */
    val description: String? get() = data.description

    /**
     * The title url, if present.
     */
    val url: String? get() = data.url

    /**
     * The timestamp, if present. Unrelated to the creation time of the embed.
     */
    val timestamp: Instant? get() = data.timestamp?.toInstant()

    /**
     * The color of the embed, if present.
     */
    val color: Color? get() = data.color?.let { Color(it, true) }

    /**
     * The footer, if present.
     */
    val footer: Footer? get() = data.footer?.let { Footer(it, kord) }

    /**
     * The image, if present.
     */
    val image: Image? get() = data.image?.let { Image(it, kord) }

    /**
     * The thumbnail, if present.
     */
    val thumbnail: Thumbnail? get() = data.thumbnail?.let { Thumbnail(it, kord) }

    /**
     * The embedded video, if present.
     */
    val video: Video? get() = data.video?.let { Video(it, kord) }

    /**
     * The embed provider, if present.
     */
    val provider: Provider? get() = data.provider?.let { Provider(it, kord) }

    /**
     * The embed author, if present.
     */
    val author: Author? get() = data.author?.let { Author(it, kord) }

    /**
     * The embed fields.
     */
    val fields: List<Field> = emptyList()

    /**
     * The type of embeds, this is an non-exhaustive list.
     */
    @Deprecated(embedDeprecationMessage)
    enum class Type(val value: String) {
        Image("image"),
        Link("link"),
        Rich("rich"),
        Video("video"),
        @Suppress("SpellCheckingInspection")
        Gifv("gifv"),
        Article("article")
    }

    data class Footer(val data: EmbedFooterData, override val kord: Kord) : KordObject {
        val text: String get() = data.text
        val iconUrl: String? get() = data.iconUrl
        val proxyIconUrl: String? get() = data.proxyIconUrl
    }

    data class Image(val data: EmbedImageData, override val kord: Kord) : KordObject {
        val url: String? get() = data.url
        val proxyUrl: String? get() = data.proxyUrl
        val height: Int? get() = data.height
        val width: Int? get() = data.width
    }

    data class Thumbnail(val data: EmbedThumbnailData, override val kord: Kord) : KordObject {
        val url: String? get() = data.url
        val proxyUrl: String? get() = data.proxyUrl
        val height: Int? get() = data.height
        val width: Int? get() = data.width
    }

    data class Video(val data: EmbedVideoData, override val kord: Kord) : KordObject {
        val url: String? get() = data.url
        val height: Int? get() = data.height
        val width: Int? get() = data.width
    }

    data class Provider(val data: EmbedProviderData, override val kord: Kord) : KordObject {
        val name: String? get() = data.name
        val url: String? get() = data.url
    }

    data class Author(val data: EmbedAuthorData, override val kord: Kord) : KordObject {
        val name: String? get() = data.name
        val url: String? get() = data.url
        val iconUrl: String? get() = data.iconUrl
        val proxyIconUrl: String? get() = data.proxyIconUrl
    }

    data class Field(val data: EmbedFieldData, override val kord: Kord) : KordObject {
        val name: String get() = data.name
        val value: String get() = data.value
        val inline: Boolean? get() = data.inline
    }

    /**
     * Applies this embed to the [builder], copying its properties to it.
     *
     * Properties that are part of this embed but not present in the [builder] will be ignored.
     */
    fun apply(builder: EmbedBuilder) {
        builder.color = color

        author?.let { author ->
            builder.author {
                this.icon = author.iconUrl
                this.url = author.url
                this.name = author.name
            }
        }

        thumbnail?.let { thumbnail ->
            builder.thumbnail {
                this.url = thumbnail.url ?: ""
            }
        }

        builder.title = title

        builder.url = url

        builder.description = description

        fields.forEach { field ->
            builder.field {
                name = field.name
                value = field.value
                inline = field.inline ?: false
            }
        }

        builder.image = image?.url

        footer?.let { footer ->
            builder.footer {
                this.text = footer.text
                this.icon = footer.iconUrl
            }
        }

        builder.timestamp = timestamp

    }
}

package com.gitlab.kordlib.core.entity

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.KordObject

/**
 * An object that is identified by its [id].
 */
interface Entity : KordObject, Comparable<Entity> {
    /**
     * The unique identifier of this entity.
     */
    val id: Snowflake

    /**
     * Compares entities on [id].
     */
    override operator fun compareTo(other: Entity): Int = comparator.compare(this, other)

    companion object {
        val comparator = compareBy<Entity> { it.id }
    }
}

package com.gitlab.kordlib.core.entity

import com.gitlab.kordlib.common.entity.*
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.GuildBehavior
import com.gitlab.kordlib.core.behavior.MemberBehavior
import com.gitlab.kordlib.core.behavior.RoleBehavior
import com.gitlab.kordlib.core.behavior.channel.GuildChannelBehavior
import com.gitlab.kordlib.core.behavior.channel.GuildMessageChannelBehavior
import com.gitlab.kordlib.core.behavior.channel.TextChannelBehavior
import com.gitlab.kordlib.core.behavior.channel.VoiceChannelBehavior
import com.gitlab.kordlib.core.cache.data.GuildData
import com.gitlab.kordlib.core.entity.channel.GuildChannel
import com.gitlab.kordlib.core.entity.channel.GuildMessageChannel
import com.gitlab.kordlib.core.entity.channel.TextChannel
import com.gitlab.kordlib.core.entity.channel.VoiceChannel
import com.gitlab.kordlib.core.exception.EntityNotFoundException
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.core.supplier.getChannelOfOrNull
import com.gitlab.kordlib.core.toSnowflakeOrNull
import com.gitlab.kordlib.rest.Image
import com.gitlab.kordlib.rest.service.RestClient
import kotlinx.coroutines.flow.first
import java.time.Instant
import java.time.format.DateTimeFormatter
import java.util.*

/**
 * An instance of a [Discord Guild](https://discord.com/developers/docs/resources/guild).
 */
class Guild(
        val data: GuildData,
        override val kord: Kord,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : GuildBehavior {

    override val id: Snowflake get() = Snowflake(data.id)

    /**
     * The id of the afk voice channel, if present.
     */
    val afkChannelId: Snowflake? get() = data.afkChannelId.toSnowflakeOrNull()

    val afkChannel: VoiceChannelBehavior?
        get() = afkChannelId?.let { VoiceChannelBehavior(guildId = id, id = it, kord = kord) }

    /**
     * The afk timeout in seconds.
     */
    val afkTimeout: Int get() = data.afkTimeout

    /**
     *  The id of the guild creator if it is bot-created.
     */
    val applicationId: Snowflake? get() = data.applicationId.toSnowflakeOrNull()

    /**
     * The approximate number of members in this guild. Present if this guild was requested through
     * [rest][com.gitlab.kordlib.rest.service.RestClient] with the flag `with_counts`.
     */
    val approximateMemberCount: Int? get() = data.approximateMemberCount

    /**
     * The approximate number of online members in this guild. Present if this guild was requested through
     * [rest][com.gitlab.kordlib.rest.service.RestClient] with the flag `with_counts`.
     */
    val approximatePresenceCount: Int? get() = data.approximatePresenceCount

    /**
     * The banner hash, if present.
     */
    val bannerHash: String? get() = data.banner

    /**
     * The ids of all [channels][GuildChannel].
     */
    val channelIds: Set<Snowflake> get() = data.channels.asSequence().map { Snowflake(it) }.toSet()

    /**
     * The explicit content filter level.
     */
    val contentFilter: ExplicitContentFilter get() = data.explicitContentFilter

    /**
     * The description of this guild, if present.
     */
    val description: String? get() = data.description

    /**
     * The ID of the embedded channel, if present.
     */
    val embedChannelId: Snowflake? get() = data.embedChannelId?.let(::Snowflake)

    /**
     * The behavior of the embedded channel, if present.
     */
    val embedChannel: GuildChannelBehavior?
        get() = embedChannelId?.let { GuildChannelBehavior(guildId = id, id = it, kord = kord) }

    /**
     * The ids of custom emojis in this guild.
     */
    val emojiIds: Set<Snowflake> get() = data.emojis.asSequence().map { it.id.toSnowflakeOrNull() }.filterNotNull().toSet()

    /**
     * The custom emojis in this guild.
     */
    val emojis: List<GuildEmoji> get() = data.emojis.map { GuildEmoji(it, kord) }

    /**
     * The behavior of the @everyone role.
     */
    val everyoneRole: RoleBehavior get() = RoleBehavior(id, id, kord)

    /**
     * The enabled guild features.
     */
    val features: Set<GuildFeature> get() = data.features.toSet()

    /**
     * The icon hash, if present.
     */
    val iconHash: String? get() = data.icon

    /**
     * The time at which this guild was joined, if present.
     */
    val joinedTime: Instant? get() = data.joinedAt?.let { DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(it, Instant::from) }

    /**
     * The id of the owner.
     */
    val ownerId: Snowflake get() = Snowflake(data.ownerId)

    /**
     * The behavior of the owner.
     */
    val owner: MemberBehavior get() = MemberBehavior(guildId = id, id = ownerId, kord = kord)

    /**
     * The number of members in the guild, if present.
     *
     *  > This value is only present when the [Kord.cache] stores guilds and the client has been logged in. It
     *  will not be updated throughout the lifetime of the gateway and should thus be seen as an approximation rather
     *  than a precise value.
     */
    val memberCount: Int? get() = data.memberCount

    /**
     * The required multi-factor authentication level of this guild.
     */
    val mfaLevel: MFALevel get() = data.mfaLevel

    /**
     * The name of this guild.
     */
    val name: String get() = data.name

    /**
     * the id of the channel where guild notices such as welcome messages and boost events are posted.
     */
    val publicUpdatesChannelId: Snowflake? get() = data.publicUpdatesChannelId.toSnowflakeOrNull()

    /**
     * The behavior of the channel where guild notices such as welcome messages and boost events are posted.
     */
    val publicUpdatesChannel: GuildMessageChannelBehavior?
        get() = publicUpdatesChannelId?.let {
            GuildMessageChannelBehavior(guildId = id, id = it, kord = kord)
        }

    val preferredLocale: Locale get() = Locale.forLanguageTag(data.preferredLocale)

    /**
     * The behaviors of all [channels][GuildChannel].
     */
    val channelBehaviors: Set<GuildChannelBehavior>
        get() = data.channels.asSequence().map { GuildChannelBehavior(id = Snowflake(it), guildId = id, kord = kord) }.toSet()

    /**
     * The default message notification level.
     */
    val defaultMessageNotificationLevel: DefaultMessageNotificationLevel get() = data.defaultMessageNotifications

    /**
     * The voice region id for the guild.
     */
    val regionId: Snowflake get() = Snowflake(data.region)

    /**
     * The id of the channel in which a discoverable server's rules should be found
     **/
    val rulesChannelId: Snowflake? get() = data.rulesChannelId.toSnowflakeOrNull()

    /**
     * The channel behavior in which a discoverable server's rules should be found.
     **/
    val rulesChannel: GuildMessageChannelBehavior?
        get() = data.rulesChannelId.toSnowflakeOrNull()?.let {
            GuildMessageChannelBehavior(id, it, kord)
        }

    /**
     * The splash hash, if present.
     */
    val splashHash: String? get() = data.splash

    /**
     * The hash of the discovery splash, if present.
     */
    val discoverySplashHash: String? get() = data.discoverySplash

    /**
     * The id of the channel to which system messages are sent.
     */
    val systemChannelId: Snowflake? get() = data.systemChannelId.toSnowflakeOrNull()

    /**
     * The behavior of the channel to which system messages are sent.
     */
    val systemChannel: TextChannelBehavior?
        get() = systemChannelId?.let {
            TextChannelBehavior(guildId = id, id = it, kord = kord)
        }

    val systemChannelFlags: SystemChannelFlags get() = data.systemChannelFlags ?: SystemChannelFlags(0)

    /**
     * The verification level required for the guild.
     */
    val verificationLevel: VerificationLevel get() = data.verificationLevel

    /**
     * The ids of the roles.
     */
    val roleIds: Set<Snowflake> get() = data.roles.asSequence().map { Snowflake(it) }.toSet()

    /**
     * The behaviors of the [roles][Role].
     */
    val roleBehaviors: Set<RoleBehavior> get() = data.roles.asSequence().map { RoleBehavior(id = Snowflake(it), guildId = id, kord = kord) }.toSet()

    /**
     * Requests to get the [VoiceChannel] represented by the [afkChannelId],
     * returns null if the [afkChannelId] isn't present or the channel itself isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the [VoiceChannel] wasn't present.
     */
    suspend fun getAfkChannel(): VoiceChannel? = afkChannelId?.let { supplier.getChannelOfOrNull(it) }

    /**
     * Gets the banner url in the specified format.
     */
    fun getBannerUrl(format: Image.Format): String? = data.banner?.let { "https://cdn.discordapp.com/banners/${id.value}/$it.${format.extension}" }

    /**
     * Requests to get the banner image in the specified [format], if present.
     */
    suspend fun getBanner(format: Image.Format): Image? {
        val url = getBannerUrl(format) ?: return null

        return Image.fromUrl(kord.resources.httpClient, url)
    }

    /**
     * Requests to get the [GuildChannel] represented by the [embedChannel],
     * returns null if the [GuildChannel] isn't present or [embedChannel] is null.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getEmbedChannel(): GuildChannel? = embedChannelId?.let { supplier.getChannelOfOrNull(it) }

    /**
     * Requests to get the [GuildEmoji] represented by the [emojiId] in this guild.
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the [GuildEmoji] wasn't present.
     */
    suspend fun getEmoji(emojiId: Snowflake): GuildEmoji =
            supplier.getEmoji(guildId = id, emojiId = emojiId)

    /**
     * Requests to get the [GuildEmoji] represented by the [emojiId] in this guild,
     * returns null if the [GuildEmoji] isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getEmojiOrNull(emojiId: Snowflake): GuildEmoji? =
            supplier.getEmojiOrNull(guildId = id, emojiId = emojiId)

    /**
     * Requests to get the `@everyone` [Role].
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the [Role] wasn't present.
     */
    suspend fun getEveryoneRole(): Role = supplier.getRole(id, id)

    /**
     * Requests to get the `@everyone` [Role],
     * returns null if the [Role] isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getEveryoneRoleOrNull(): Role? = supplier.getRoleOrNull(id, id)

    /**
     * Gets the discovery splash url in the specified [format], if present.
     */
    fun getDiscoverySplashUrl(format: Image.Format): String? =
            data.splash?.let { "discovery-splashes/${id.value}/${it}.${format.extension}" }

    /**
     * Requests to get the splash image in the specified [format], if present.
     *
     * This property is not resolvable through cache and will always use the [RestClient] instead.
     */
    suspend fun getDiscoverySplash(format: Image.Format): Image? {
        val url = getDiscoverySplashUrl(format) ?: return null

        return Image.fromUrl(kord.resources.httpClient, url)
    }

    /**
     * Gets the icon url, if present.
     */
    fun getIconUrl(format: Image.Format): String? = data.icon?.let { "https://cdn.discordapp.com/icons/${id.value}/$it.${format.extension}" }

    /**
     * Requests to get the icon image in the specified [format], if present.
     */
    suspend fun getIcon(format: Image.Format): Image? {
        val url = getIconUrl(format) ?: return null

        return Image.fromUrl(kord.resources.httpClient, url)
    }

    /**
     * Requests to get the owner of this guild as a [Member].
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the [Member] wasn't present.
     */
    suspend fun getOwner(): Member = supplier.getMember(id, ownerId)

    /**
     * Requests to get The channel where guild notices such as welcome messages and boost events are posted.
     */
    suspend fun getPublicUpdatesChannel(): GuildMessageChannel? = publicUpdatesChannel?.asChannel()

    /**
     * Requests to get the owner of this guild as a [Member],
     * returns null if the [Member] isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getOwnerOrNull(): Member? = supplier.getMemberOrNull(id, ownerId)

    /**
     * Requests to get the [voice region][Region] of this guild.
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the [Region] wasn't present.
     * @throws [NoSuchElementException] if the [regionId] is not in the available [regions].
     */
    suspend fun getRegion(): Region = regions.first { it.id == regionId }

    /**
     * Requests to get the the channel in which a discoverable server's rules should be found represented
     *, returns null if the [GuildMessageChannel] isn't present, or [rulesChannelId] is null.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getRulesChannel(): GuildMessageChannel? = rulesChannel?.asChannel()

    /**
     * Gets the splash url in the specified [format], if present.
     */
    fun getSplashUrl(format: Image.Format): String? =
            data.splash?.let { "https://cdn.discordapp.com/splashes/${id.value}/$it.${format.extension}" }

    /**
     * Requests to get the splash image in the specified [format], if present.
     */
    suspend fun getSplash(format: Image.Format): Image? {
        val url = getSplashUrl(format) ?: return null

        return Image.fromUrl(kord.resources.httpClient, url)
    }

    /**
     * Requests to get the channel where system messages (member joins, server boosts, etc),
     * returns null if the [TextChannel] isn't present or the [systemChannelId] is null.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getSystemChannel(): TextChannel? =
            systemChannelId?.let { supplier.getChannelOfOrNull(it) }

    /**
     * Returns a new [Guild] with the given [strategy].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>): Guild = Guild(data, kord, strategy.supply(kord))

    override fun hashCode(): Int = Objects.hash(id)

    override fun equals(other: Any?): Boolean = when (other) {
        is GuildBehavior -> other.id == id
        else -> false
    }
}

package com.gitlab.kordlib.core.entity

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.MemberBehavior
import com.gitlab.kordlib.core.behavior.RoleBehavior
import com.gitlab.kordlib.core.behavior.UserBehavior
import com.gitlab.kordlib.core.cache.data.EmojiData
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.core.toSnowflakeOrNull
import com.gitlab.kordlib.rest.builder.guild.EmojiModifyBuilder
import io.ktor.utils.io.bits.withMemory
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter
import java.util.*
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

/**
 * An instance of a [Discord emoji](https://discord.com/developers/docs/resources/emoji#emoji-object) belonging to a specific guild.
 */
class GuildEmoji(
        val data: EmojiData,
        override val kord: Kord,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : Entity, Strategizable {

    override val id: Snowflake
        get() = Snowflake(data.id)

    val guildId: Snowflake
        get() = Snowflake(data.guildId)

    val mention: String
        get() = if (isAnimated) "<a:$name:${id.value}>" else "<:$name:${id.value}>"

    /**
     * Whether this emoji can be used, may be false due to loss of Server Boosts.
     */
    val isAvailable: Boolean get() = data.available

    /**
     * Whether is emoji is animated.
     */
    val isAnimated: Boolean get() = data.animated

    /**
     * Whether is emote is managed by Discord instead of the guild members.
     */
    val isManaged: Boolean get() = data.managed

    /**
     * The name of this emoji.
     *
     * This property can be null when trying to get the name of an emoji that was deleted.
     */
    val name: String? get() = data.name

    /**
     * Whether this emoji needs to be wrapped in colons.
     */
    val requiresColons: Boolean get() = data.requireColons

    /**
     * The ids of the [roles][Role] for which this emoji was whitelisted.
     */
    val roleIds: Set<Snowflake> get() = data.roles.asSequence().map { Snowflake(it) }.toSet()

    /**
     * The behaviors of the [roles][Role] for which this emoji was whitelisted.
     */
    val roleBehaviors: Set<RoleBehavior> get() = data.roles.asSequence().map { RoleBehavior(guildId = guildId, id = id, kord = kord) }.toSet()

    /**
     * The [roles][Role] for which this emoji was whitelisted.
     *
     * This request uses state [data] to resolve the entities belonging to the flow,
     * as such it can't guarantee an up to date representation if the [data] is outdated.
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    val roles: Flow<Role>
        get() = if (roleIds.isEmpty()) emptyFlow()
        else supplier.getGuildRoles(guildId).filter { it.id in roleIds }

    /**
     * The behavior of the [Member] who created the emote, if present.
     */
    val member: MemberBehavior? get() = userId?.let { MemberBehavior(guildId, it, kord) }

    /**
     * The id of the [User] who created the emote, if present.
     */
    val userId: Snowflake? get() = data.user?.id.toSnowflakeOrNull()

    /**
     * The [User] who created the emote, if present.
     */
    val user: UserBehavior? get() = userId?.let { UserBehavior(it, kord) }

    /**
     * Requests to delete this emoji, with the given [reason].
     *
     * @throws RequestException if anything went wrong during the request.
     */
    suspend fun delete(reason: String? = null) {
        kord.rest.emoji.deleteEmoji(guildId = guildId.value, emojiId = id.value, reason = reason)
    }

    /**
     *  Requests to edit the emoji.
     *
     *  @throws [RequestException] if anything went wrong during the request.
     */
    @OptIn(ExperimentalContracts::class)
    suspend inline fun edit(builder: EmojiModifyBuilder.() -> Unit) {
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }
        kord.rest.emoji.modifyEmoji(guildId = guildId.value, emojiId = id.value, builder = builder)
    }

    /**
     * Requests to get the creator of the emoji as a [Member],
     * returns null if the [Member] isn't present or [userId] is null.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getMember(): Member? = userId?.let { supplier.getMemberOrNull(guildId = guildId, userId = it) }

    /**
     * Requests to get the creator of the emoji as a [User],
     * returns null if the [User] isn't present or [userId] is null.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getUser(): User? = userId?.let { supplier.getUserOrNull(it) }

    /**
     * Returns a new [GuildEmoji] with the given [strategy].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>) = GuildEmoji(data, kord, strategy.supply(kord))

    override fun hashCode(): Int = Objects.hash(id, guildId)

    override fun equals(other: Any?): Boolean = when (other) {
        is GuildEmoji -> other.id == id && other.guildId == guildId
        else -> super.equals(other)
    }

}

package com.gitlab.kordlib.core.entity

import com.gitlab.kordlib.common.entity.GuildFeature
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.cache.data.GuildPreviewData
import com.gitlab.kordlib.core.supplier.EntitySupplier

class GuildPreview(
        val data: GuildPreviewData,
        override val kord: Kord
) : Entity {

    override val id: Snowflake
        get() = Snowflake(data.id)

    /**
     * The name of the guild
     */
    val name: String get() = data.name

    /**
     * Icon hash.
     */
    val icon: String? get() = data.icon

    /**
     * Splash hash.
     */
    val splash: String? get() = data.splash

    /**
     * Discovery splash hash.
     */
    val discoverySplash: String? get() = data.discoverySplash

    /**
     * Ids of custom guild emojis.
     */
    val emojis: Set<GuildEmoji> get() = data.emojis.map { GuildEmoji(it, kord) }.toSet()

    /**
     * Enabled guild features.
     */
    val features: Set<GuildFeature> get() = data.features.toSet()

    /**
     * Approximate number of members in this guild.
     */
    val approximateMemberCount: Int get() =  data.approximateMemberCount

    /**
     * Approximate number of online members in this guild.
     */
    val approximatePresenceCount: Int get() = data.approximatePresenceCount

    /**
     * The description for the guild.
     */
    val description: String? get() = data.description

}

package com.gitlab.kordlib.core.entity

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.GuildBehavior
import com.gitlab.kordlib.core.behavior.RoleBehavior
import com.gitlab.kordlib.core.behavior.UserBehavior
import com.gitlab.kordlib.core.cache.data.IntegrationData
import com.gitlab.kordlib.core.exception.EntityNotFoundException
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.core.toInstant
import com.gitlab.kordlib.rest.builder.integration.IntegrationModifyBuilder
import com.gitlab.kordlib.rest.json.response.IntegrationExpireBehavior
import com.gitlab.kordlib.rest.request.RestRequestException
import java.time.Duration
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.*
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

/**
 * A [Discord integration](https://discord.com/developers/docs/resources/guild#get-guild-integrations).
 */
class Integration(
        val data: IntegrationData,
        override val kord: Kord,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : Entity, Strategizable {

    override val id: Snowflake
        get() = Snowflake(data.id)

    /**
     * The name of this integration.
     */
    val name: String
        get() = data.name

    /**
     * The type of integration. (`"twitch"`, `"youtube"`, etc)
     */
    val type: String
        get() = data.type

    /**
     * Whether this integration is currently active.
     */
    val isEnabled: Boolean
        get() = data.enabled

    /**
     * Whether this integrations is syncing.
     */
    val isSyncing: Boolean
        get() = data.syncing

    /**
     * The id of the [guild][Guild] this integration is tied to.
     */
    val guildId: Snowflake
        get() = Snowflake(data.guildId)

    /**
     * The behavior of the [guild][Guild] this integration is tied to.
     */
    val guild: GuildBehavior
        get() = GuildBehavior(id = guildId, kord = kord)

    /**
     * The id of the [role][Role] used for 'subscribers' of the integration.
     */
    val roleId: Snowflake
        get() = Snowflake(data.id)

    /**
     * The behavior of the [role][Role] used for 'subscribers' of the integration.
     */
    val role: RoleBehavior
        get() = RoleBehavior(guildId = guildId, id = roleId, kord = kord)

    /**
     * Whether this integration requires emoticons to be synced, only supports Twitch right now.
     */
    val enablesEmoticons: Boolean
        get() = data.enableEmoticons

    /**
     * The behavior used to expire subscribers.
     */
    val expireBehavior: IntegrationExpireBehavior
        get() = data.expireBehavior

    /**
     * The grace period in days before expiring subscribers.
     */
    val expireGracePeriod: Duration
        get() = Duration.of(data.expireGracePeriod.toLong(), ChronoUnit.DAYS)

    /**
     * The id of the [user][User] for this integration.
     */
    val userId: Snowflake
        get() = Snowflake(data.id)

    /**
     * The behavior of the [user][User] for this integration.
     */
    val user: UserBehavior
        get() = UserBehavior(id = userId, kord = kord)

    /**
     * When this integration was last synced.
     */
    val syncedAt: Instant
        get() = data.syncedAt.toInstant()

    /**
     * Requests to get the guild this integration is tied to.
     *
     * @throws [RequestException] if something went wrong during the request.
     * @throws [EntityNotFoundException] if the guild isn't present.
     */
    suspend fun getGuild(): Guild = supplier.getGuild(guildId)

    /**
     * Requests to get the guild this integration is tied to, returns null if the guild isn't present.
     *
     * @throws [RequestException] if something went wrong during the request.
     */
    suspend fun getGuildOrNull(): Guild? = supplier.getGuildOrNull(guildId)

    /**
     * Requests to get the role used for 'subscribers' of the integration.
     *
     * @throws [RequestException] if something went wrong during the request.
     * @throws [EntityNotFoundException] if the role isn't present.
     */
    suspend fun getRole(): Role = supplier.getRole(guildId = guildId, roleId = roleId)

    /**
     * Requests to get the role used for 'subscribers' of the integration,
     * returns null if the role isn't present.
     *
     * @throws [RequestException] if something went wrong during the request.
     */
    suspend fun getRoleOrNull(): Role? = supplier.getRoleOrNull(guildId = guildId, roleId = roleId)

    /**
     * Requests to delete the integration.
     */
    suspend fun delete() {
        kord.rest.guild.deleteGuildIntegration(guildId = guildId.value, integrationId = id.value)
    }

    /**
     * Request to sync an integration.
     */
    suspend fun sync() = kord.rest.guild.syncGuildIntegration(guildId = guildId.value, integrationId = id.value)

    override fun withStrategy(strategy: EntitySupplyStrategy<*>): Integration =
            Integration(data, kord, strategy.supply(kord))

    override fun hashCode(): Int = Objects.hash(id)

    override fun equals(other: Any?): Boolean = when (other) {
        is Integration -> other.id == id && other.guildId == guildId
        else -> false
    }
}

/**
 * Requests to edit this integration.
 *
 * @return The edited [Integration].
 *
 * @throws [RestRequestException] if something went wrong during the request.
 */
@OptIn(ExperimentalContracts::class)
suspend inline fun Integration.edit(builder: IntegrationModifyBuilder.() -> Unit) {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    kord.rest.guild.modifyGuildIntegration(guildId.value, id.value, builder)
}

package com.gitlab.kordlib.core.entity

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.entity.TargetUserType
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.KordObject
import com.gitlab.kordlib.core.behavior.UserBehavior
import com.gitlab.kordlib.core.behavior.channel.ChannelBehavior
import com.gitlab.kordlib.core.cache.data.InviteData
import com.gitlab.kordlib.core.entity.channel.Channel
import com.gitlab.kordlib.core.exception.EntityNotFoundException
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.core.supplier.getChannelOf
import com.gitlab.kordlib.core.supplier.getChannelOfOrNull
import com.gitlab.kordlib.core.toSnowflakeOrNull

/**
 * An instance of a [Discord Invite](https://discord.com/developers/docs/resources/invite).
 */
data class Invite(
        val data: InviteData,
        override val kord: Kord,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : KordObject, Strategizable {

    /**
     * The unique code of this invite.
     */
    val code: String get() = data.code

    /**
     * The id of the channel this invite is associated to.
     */
    val channelId: Snowflake get() = Snowflake(data.channelId)

    /**
     * Returns [PartialGuild] if the invite was made in a guild, or null if not.
     */
    val partialGuild: PartialGuild? get() = data.guild?.let { PartialGuild(it, kord) }

    /**
     * The id of the [Guild] if the invite was made in a guild, or null if not.
     */
    @Deprecated("Moved to the PartialGuild", ReplaceWith("partialGuild?.id"))

    val guildId: Snowflake? get() = partialGuild?.id

    /**
     * The id of the user who created this invite, if present.
     */
    val inviterId: Snowflake? get() = data.inviterId.toSnowflakeOrNull()

    /**
     * The id of the target user this invite is associated to, if present.
     */
    val targetUserId: Snowflake? get() = data.targetUserId.toSnowflakeOrNull()

    /**
     * The behavior of the channel this invite is associated to.
     */
    val channel: ChannelBehavior get() = ChannelBehavior(channelId, kord)

    /**
     * The user behavior of the user who created this invite, if present.
     */
    val inviter: UserBehavior? get() = inviterId?.let { UserBehavior.invoke(it, kord) }

    /**
     * The user behavior of the target user this invite is associated to, if present.
     */
    val targetUser: UserBehavior? get() = targetUserId?.let { UserBehavior(it, kord) }

    /**
     * The type of user target for this invite, if present.
     */
    val targetUserType: TargetUserType? get() = data.targetUserType

    /**
     * Approximate count of members in the channel this invite is associated to, if present.
     */
    val approximateMemberCount: Int? get() = data.approximateMemberCount

    /**
     * Approximate count of members online in the channel this invite is associated to, if present. (only present when the target user isn't null)
     */
    val approximatePresenceCount: Int? get() = data.approximatePresenceCount

    /**
     * Requests to get the channel this invite is for.
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the [Channel] wasn't present.
     */
    suspend fun getChannel(): Channel = supplier.getChannelOf(channelId)

    /**
     * Requests to get the channel this invite is for,
     * returns null if the [Channel] isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getChannelOrNull(): Channel? = supplier.getChannelOfOrNull(channelId)

    /**
     * Requests to get the creator of the invite for,
     * returns null if the [User] isn't present or [inviterId] is null.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getInviter(): User? = inviterId?.let { supplier.getUserOrNull(it) }

    /**
     * Requests to get the user this invite was created for,
     * returns null if the [User] isn't present or [targetUserId] is null.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getTargetUser(): User? = targetUserId?.let { supplier.getUserOrNull(it) }

    /**
     * Requests to delete the invite.
     */
    suspend fun delete(reason: String? = null) {
        kord.rest.invite.deleteInvite(data.code, reason)
    }

    /**
     * Returns a new [Invite] with the given [strategy].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>): Invite =
            Invite(data, kord, strategy.supply(kord))

}

package com.gitlab.kordlib.core.entity

import com.gitlab.kordlib.common.entity.Permission
import com.gitlab.kordlib.common.entity.Permissions
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.MemberBehavior
import com.gitlab.kordlib.core.behavior.RoleBehavior
import com.gitlab.kordlib.core.behavior.UserBehavior
import com.gitlab.kordlib.core.cache.data.MemberData
import com.gitlab.kordlib.core.cache.data.UserData
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.core.toInstant
import kotlinx.coroutines.flow.*
import java.time.Instant
import java.time.format.DateTimeFormatter
import java.util.*

/**
 * An instance of a [Discord Member](https://discord.com/developers/docs/resources/guild#guild-member-object).
 */
class Member(
        val memberData: MemberData,
        userData: UserData,
        kord: Kord,
        supplier: EntitySupplier = kord.defaultSupplier
) : User(userData, kord, supplier), MemberBehavior {

    override val guildId: Snowflake
        get() = Snowflake(memberData.guildId)

    /**
     * The name as shown in the discord client, prioritizing the [nickname] over the [use].
     */
    val displayName: String get() = nickname ?: username

    /**
     * When the user joined this [guild].
     */
    val joinedAt: Instant get() = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(memberData.joinedAt, Instant::from)

    /**
     * The guild-specific nickname of the user, if present.
     */
    val nickname: String? get() = memberData.nick

    /**
     * When the user used their Nitro boost on the server.
     */
    val premiumSince: Instant? get() = memberData.premiumSince?.toInstant()

    /**
     * The ids of the [roles][Role] that apply to this user.
     */
    val roleIds: Set<Snowflake> get() = memberData.roles.asSequence().map { Snowflake(it) }.toSet()

    /**
     * The behaviors of the [roles][Role] that apply to this user.
     */
    val roleBehaviors: Set<RoleBehavior>
        get() = memberData.roles.asSequence().map { RoleBehavior(guildId = guildId, id = Snowflake(it), kord = kord) }.toSet()

    /**
     * The [roles][Role] that apply to this user.
     *
     * This request uses state [data] to resolve the entities belonging to the flow,
     * as such it can't guarantee an up to date representation if the [data] is outdated.
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    val roles: Flow<Role>
        get() = if (roleIds.isEmpty()) emptyFlow()
        else supplier.getGuildRoles(guildId).filter { it.id in roleIds }

    /**
     * Whether this member's [id] equals the [Guild.ownerId].
     *
     * @throws [RequestException] if something went wrong during the request.
     */
    suspend fun isOwner(): Boolean = getGuild().ownerId == id

    /**
     * Requests to calculate a summation of the permissions of this member's [roles].
     *
     * @throws [RequestException] if something went wrong during the request.
     */
    suspend fun getPermissions(): Permissions {
        val guild = getGuild()
        val owner = guild.ownerId == this.id
        if (owner) return Permissions {
            +Permission.All
        }

        val everyone = guild.getEveryoneRole().permissions
        val roles = roles.map { it.permissions }.toList()

        return Permissions {
            +everyone
            roles.forEach { +it }
        }
    }

    /**
     * Returns a new [Member] with the given [strategy].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>): Member = Member(memberData, data, kord, strategy.supply(kord))

    override fun hashCode(): Int = Objects.hash(id, guildId)

    override fun equals(other: Any?): Boolean = when(other) {
        is MemberBehavior -> other.id == id && other.guildId == guildId
        is UserBehavior -> other.id == id
        else -> false
    }

}

package com.gitlab.kordlib.core.entity

import com.gitlab.kordlib.common.entity.MessageType
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.GuildBehavior
import com.gitlab.kordlib.core.behavior.MessageBehavior
import com.gitlab.kordlib.core.behavior.RoleBehavior
import com.gitlab.kordlib.core.behavior.UserBehavior
import com.gitlab.kordlib.core.behavior.channel.ChannelBehavior
import com.gitlab.kordlib.core.cache.data.MessageData
import com.gitlab.kordlib.core.entity.channel.*
import com.gitlab.kordlib.core.exception.EntityNotFoundException
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.core.supplier.getChannelOf
import com.gitlab.kordlib.core.supplier.getChannelOfOrNull
import com.gitlab.kordlib.core.toSnowflakeOrNull
import kotlinx.coroutines.flow.*
import java.time.Instant
import java.time.format.DateTimeFormatter
import java.util.*

/**
 * An instance of a [Discord Message][https://discord.com/developers/docs/resources/channel#message-object].
 */
class Message(
        val data: MessageData,
        override val kord: Kord,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : MessageBehavior {

    /**
     * The id of this message.
     */
    override val id: Snowflake
        get() = Snowflake(data.id)

    /**
     * The id of the [MessageChannel] this message was send in.
     */
    override val channelId: Snowflake
        get() = Snowflake(data.channelId)

    /**
     * The files attached to this message.
     */
    val attachments: Set<Attachment> get() = data.attachments.asSequence().map { Attachment(it, kord) }.toSet()

    /**
     * The author of this message, if it was created by a [User].
     *
     * Returns null if the author is not a Discord account, like a [Webhook] or systems message.
     */
    val author: User?
        get() = if (data.webhookId == data.author.id) null
        else User(data.author, kord)

    /**
     * The content of this message.
     */
    val content: String get() = data.content

    /**
     * The instant when this message was last edited, if ever.
     *
     * Returns null if the message was never edited.
     */
    val editedTimestamp: Instant?
        get() = data.editedTimestamp?.let {
            DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(it, Instant::from)
        }

    /**
     * The embedded content of this message.
     *
     * This includes automatically embedded [videos][Embed.video] and [urls][Embed.Provider].
     */
    val embeds: List<Embed> get() = data.embeds.map { Embed(it, kord) }

    /**
     * The ids of [Channels][Channel] specifically mentioned in this message.
     *
     * This collection can only contain values on crossposted messages, channels
     * mentioned inside the same guild will not be present.
     */
    val mentionedChannelIds: Set<Snowflake> get() = data.mentionedChannels.orEmpty().map { Snowflake(it) }.toSet()

    /**
     * The [Channels][ChannelBehavior] specifically mentioned in this message.
     *
     * This collection can only contain values on crossposted messages, channels
     * mentioned inside the same guild will not be present.
     */
    val mentionedChannelBehaviors: Set<ChannelBehavior> get() = data.mentionedChannels.orEmpty().map { ChannelBehavior(Snowflake(it), kord) }.toSet()

    /**
     * The [Channels][Channel] specifically mentioned in this message.
     *
     * This property will only emit values on crossposted messages, channels
     * mentioned inside the same guild will not be present.
     *
     * This request uses state [data] to resolve the entities belonging to the flow,
     * as such it can't guarantee an up to date representation if the [data] is outdated.
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    val mentionedChannels: Flow<Channel>
        get() = mentionedChannelIds.asFlow().map { supplier.getChannel(it) }

    /**
     * True if this message mentions `@everyone`.
     */
    val mentionsEveryone: Boolean get() = data.mentionEveryone

    /**
     * The [ids][Role.id] of roles mentioned in this message.
     */
    val mentionedRoleIds: Set<Snowflake> get() = data.mentionRoles.map { Snowflake(it) }.toSet()

    /**
     * The [roles][Role] mentioned in this message.
     *
     * This request uses state [data] to resolve the entities belonging to the flow,
     * as such it can't guarantee an up to date representation if the [data] is outdated.
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    val mentionedRoles: Flow<Role>
        get() = flow {
            if (mentionedRoleIds.isEmpty()) return@flow

            val guild = getGuild()
            supplier.getGuildRoles(guild.id).filter { it.id in mentionedRoleIds }
        }

    /**
     * The [ids][User.id] of users mentioned in this message.
     */
    val mentionedUserIds: Set<Snowflake> get() = data.mentions.map { Snowflake(it) }.toSet()

    /**
     * The [Behaviors][UserBehavior] of users mentioned in this message.
     */
    val mentionedUserBehaviors: Set<UserBehavior> get() = data.mentions.map { UserBehavior(Snowflake(it), kord) }.toSet()

    /**
     * The [users][User] mentioned in this message.
     *
     * This request uses state [data] to resolve the entities belonging to the flow,
     * as such it can't guarantee an up to date representation if the [data] is outdated.
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    val mentionedUsers: Flow<User>
        get() = data.mentions.asFlow().map { supplier.getUser(Snowflake(it)) }

    /**
     * Whether the message was pinned in its [channel].
     */
    val isPinned get() = data.pinned

    /**
     * The reactions to this message.
     */
    val reactions: Set<Reaction> get() = data.reactions.orEmpty().asSequence().map { Reaction(it, kord) }.toSet()

    /**
     * The instant when this message was created.
     */
    val timestamp: Instant get() = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(data.timestamp, Instant::from)

    /**
     * Whether this message was send using `\tts`.
     */
    val tts: Boolean get() = data.tts

    /**
     * The type of this message.
     */
    val type: MessageType get() = data.type

    /**
     * The [id][Webhook.id] of the [Webhook] that was used to send this message.
     *
     * Returns null if this message was not send using a webhook.
     */
    val webhookId: Snowflake? get() = data.webhookId?.let(::Snowflake)

    /**
     * Returns itself.
     */
    override suspend fun asMessage(): Message = this

    /**
     * Requests to get the channel this message was send in.
     */
    suspend fun getChannel(): MessageChannel = supplier.getChannelOf(channelId)

    suspend fun getChannelOrNull(): MessageChannel? = supplier.getChannelOfOrNull(channelId)

    /**
     * Requests to get the [author] as a member.
     *
     * Returns null if the message was not send in a [GuildMessageChannel], or if the [author] is not a [User].
     */
    suspend fun getAuthorAsMember(): Member? {
        val author = author ?: return null
        val guildId = getGuild().id
        return author.asMember(guildId)
    }

    /**
     * Requests to get the guild of this message.
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the [Guild] wasn't present.
     * @throws [ClassCastException] if this message wasn't made in a guild.
     */
    suspend fun getGuild(): Guild = supplier.getChannelOf<GuildChannel>(channelId).getGuild()

    /**
     * Requests to get the guild of this message,
     * returns null if the [Guild] isn't present or this message wasn't made in a guild.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getGuildOrNull(): Guild? = supplier.getChannelOfOrNull<GuildChannel>(channelId)?.getGuildOrNull()

    /**
     * Returns a new [Message] with the given [strategy].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>): Message = Message(data, kord, strategy.supply(kord))

    override fun hashCode(): Int = Objects.hash(id)

    override fun equals(other: Any?): Boolean = when (other) {
        is MessageBehavior -> other.id == id && other.channelId == channelId
        else -> false
    }

}

package com.gitlab.kordlib.core.entity

import com.gitlab.kordlib.common.entity.Permissions
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.GuildBehavior
import com.gitlab.kordlib.core.cache.data.PartialGuildData
import com.gitlab.kordlib.core.entity.channel.Channel
import com.gitlab.kordlib.core.exception.EntityNotFoundException
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.core.supplier.getChannelOf
import com.gitlab.kordlib.core.supplier.getChannelOfOrNull
import com.gitlab.kordlib.rest.Image
import java.util.*

class PartialGuild(
        val data: PartialGuildData,
        override val kord: Kord,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : Entity, Strategizable {

    /**
     * The name of this guild.
     */
    val name: String get() = data.name

    override val id: Snowflake get() = Snowflake(data.id)

    /**
     * The icon hash, if present.
     */
    val iconHash: String? get() = data.icon

    /**
     * wither who created the invite is the owner or not.
     */

    val owner: Boolean? get() = data.owner

    /**
     * permissions that the invite creator has, if present.
     */

    val permissions: Permissions? get() = data.permissions

    /**
     * Gets the icon url, if present.
     */
    fun getIconUrl(format: Image.Format): String? = data.icon?.let { "https://cdn.discordapp.com/icons/${id.value}/$it.${format.extension}" }

    /**
     * Requests to get the icon image in the specified [format], if present.
     */
    suspend fun getIcon(format: Image.Format): Image? {
        val url = getIconUrl(format) ?: return null

        return Image.fromUrl(kord.resources.httpClient, url)
    }

    /**
     * Requests to get the full [Guild] entity  for this [PartialGuild].
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the [Guild] wasn't present, or the bot is not a part of this [Guild].
     */
    suspend fun getGuild(): Guild = supplier.getGuild(id)

    /**
     * Requests to get the [Guild] for this invite,
     * returns null if the [Guild] isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getGuildOrNull(): Guild? = supplier.getGuildOrNull(id)

    override fun hashCode(): Int = Objects.hash(id)

    override fun equals(other: Any?): Boolean = when (other) {
        is GuildBehavior -> other.id == id
        is PartialGuild -> other.id == id
        else -> false
    }

    override fun withStrategy(strategy: EntitySupplyStrategy<*>): PartialGuild =
            PartialGuild(data, kord, strategy.supply(kord))

}

package com.gitlab.kordlib.core.entity

import com.gitlab.kordlib.common.entity.Overwrite
import com.gitlab.kordlib.common.entity.Permissions
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.cache.data.PermissionOverwriteData
import com.gitlab.kordlib.rest.json.request.ChannelPermissionEditRequest

open class PermissionOverwrite constructor(
        val data: PermissionOverwriteData
) {
    val allowed: Permissions get() = Permissions(data.allowed)
    val denied: Permissions get() = Permissions(data.denied)
    val target: Snowflake get() = Snowflake(data.id)
    val type: Type get() = Type.from(data.type)

    internal fun asRequest() = ChannelPermissionEditRequest(allowed, denied, type.value)

    internal fun toOverwrite() = Overwrite(id = target.value, type = type.value, allow = allowed.code, deny = denied.code)

    override fun hashCode(): Int = target.hashCode()
    override fun equals(other: Any?): Boolean {
        val otherOverwrite = other as? PermissionOverwrite ?: return false

        return target == otherOverwrite.target
    }

    companion object {
        fun forEveryone(guildId: Snowflake, allowed: Permissions = Permissions(), denied: Permissions = Permissions()) =
                PermissionOverwrite(PermissionOverwriteData(guildId.longValue, Type.Role.value, allowed.code, denied.code))

        fun forMember(memberId: Snowflake, allowed: Permissions = Permissions(), denied: Permissions = Permissions()) =
                PermissionOverwrite(PermissionOverwriteData(memberId.longValue, Type.Member.value, allowed.code, denied.code))

        fun forRole(roleId: Snowflake, allowed: Permissions = Permissions(), denied: Permissions = Permissions()) =
                PermissionOverwrite(PermissionOverwriteData(roleId.longValue, Type.Role.value, allowed.code, denied.code))
    }

    sealed class Type(val value: String) {
        object Role : Type("role")

        object Member : Type("member")

        companion object {
            fun from(type: String) = when (type) {
                "role" -> Role
                "member" -> Member
                else -> throw IllegalArgumentException("unknown PermissionOverwrite.Type: $type")
            }
        }
    }
}

package com.gitlab.kordlib.core.entity

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.KordObject
import com.gitlab.kordlib.core.behavior.GuildBehavior
import com.gitlab.kordlib.core.behavior.channel.GuildChannelBehavior
import com.gitlab.kordlib.core.cache.data.PermissionOverwriteData
import com.gitlab.kordlib.core.entity.channel.GuildChannel
import com.gitlab.kordlib.core.exception.EntityNotFoundException
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.core.supplier.getChannelOf
import com.gitlab.kordlib.core.supplier.getChannelOfOrNull
import com.gitlab.kordlib.rest.request.RestRequestException

class PermissionOverwriteEntity(
        val guildId: Snowflake,
        val channelId: Snowflake,
        data: PermissionOverwriteData,
        override val kord: Kord,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : PermissionOverwrite(data), KordObject, Strategizable {

    val guild: GuildBehavior get() = GuildBehavior(guildId, kord)

    val channel: GuildChannelBehavior get() = GuildChannelBehavior(guildId, channelId, kord)

    /**
     * Requests to get the channel this overwrite applies to.
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the [GuildChannel] wasn't present.
     */
    suspend fun getChannel(): GuildChannel = supplier.getChannelOf(channelId)

    /**
     * Requests to get the channel this overwrite applies to,
     * returns null if the [GuildChannel] isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getChannelOrNull(): GuildChannel? = supplier.getChannelOfOrNull(channelId)

    /**
     * Requests to get the the guild of this overwrite.
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the [Guild] wasn't present.
     */
    suspend fun getGuild(): Guild = supplier.getGuild(guildId)

    /**
     * Requests to get the guild of this overwrite,
     * returns null if the [Guild] isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getGuildOrNull(): Guild? = supplier.getGuildOrNull(guildId)

    /**
     * Requests to delete this overwrite.
     *
     * @param reason an optional reason to be logged in the audit log.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun delete(reason: String? = null) {
        kord.rest.channel.deleteChannelPermission(channelId.value, data.id.toString(), reason)
    }

    /**
     * Returns a new [PermissionOverwriteEntity] with the given [strategy].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>): PermissionOverwriteEntity =
            PermissionOverwriteEntity(guildId, channelId, data, kord, strategy.supply(kord))

}

package com.gitlab.kordlib.core.entity

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.entity.Status
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.KordObject
import com.gitlab.kordlib.core.cache.data.ClientStatusData
import com.gitlab.kordlib.core.cache.data.PresenceData
import com.gitlab.kordlib.core.exception.EntityNotFoundException
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow

class Presence(
        val data: PresenceData,
        override val kord: Kord,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : KordObject, Strategizable {

    val activities: List<Activity> get() = data.activities.map { Activity(it) }

    val clientStatus: ClientStatus get() = ClientStatus(data.clientStatus)

    val game: Activity? get() = data.game?.let { Activity(it) }

    val guildId: Snowflake? get() = data.guildId?.let { Snowflake(it) }

    val roleIds: Set<Snowflake>? get() = data.roles?.asSequence()!!.map { Snowflake(it) }.toSet()

    val roles: Flow<Role>
        get() = if (guildId == null) emptyFlow()
        else supplier.getGuildRoles(guildId!!)

    val status: Status get() = data.status

    val userId: Snowflake get() = Snowflake(data.userId)

    /**
     * Requests to get the user of this presence.
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the [User] wasn't present.
     */
    suspend fun getUser(): User = supplier.getUser(userId)

    /**
     * Requests to get the user of this presence,
     * returns null if the [User] isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getUserOrNull(): User? = supplier.getUserOrNull(userId)

    /**
     * Returns a new [Presence] with the given [strategy].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>): Presence =
            Presence(data, kord, strategy.supply(kord))

}

class ClientStatus(val data: ClientStatusData) {
    val desktop: Client.Desktop? get() = data.desktop?.let { Client.Desktop(it) }
    val mobile: Client.Mobile? get() = data.mobile?.let { Client.Mobile(it) }
    val web: Client.Web? get() = data.web?.let { Client.Web(it) }

    sealed class Client(val status: Status) {
        class Desktop(status: Status) : Client(status)
        class Mobile(status: Status) : Client(status)
        class Web(status: Status) : Client(status)
    }
}

package com.gitlab.kordlib.core.entity

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.KordObject
import com.gitlab.kordlib.core.cache.data.ReactionData
import com.gitlab.kordlib.core.toSnowflakeOrNull

/**
 * An instance of a [Discord Reaction](https://discord.com/developers/docs/resources/channel#reaction-object).
 */
class Reaction(val data: ReactionData, override val kord: Kord) : KordObject {

    /**
     *
     */
    val id: Snowflake? get() = data.emojiId.toSnowflakeOrNull()

    /**
     * The amount of users that reacted this emoji to the message.
     */
    val count: Int get() = data.count

    /**
     * Whether the current user reacted to the message with this emoji.
     */
    val selfReacted: Boolean get() = data.me

    /**
     * The emoji of this reaction.
     */
    val emoji: ReactionEmoji
        get() = when (data.emojiId) {
            null -> ReactionEmoji.Unicode(data.emojiName!!)
            else -> ReactionEmoji.Custom(Snowflake(data.emojiId), data.emojiName ?: "", data.emojiAnimated)
        }

    /**
     * Whether the emoji is animated.
     */
    val isAnimated: Boolean get() = data.emojiAnimated

}

package com.gitlab.kordlib.core.entity

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.cache.data.RemovedReactionData

sealed class ReactionEmoji {
    /**
     * Format used in HTTP queries.
     */
    abstract val urlFormat: String

    /**
     * Either the unicode representation if it's a [Unicode] emoji or the emoji name if it's a [Custom] emoji.
     */
    abstract val name: String

    abstract val mention: String

    data class Custom(val id: Snowflake, override val name: String, val isAnimated: Boolean) : ReactionEmoji() {

        override val urlFormat: String
            get() = "$name:${id.value}"

        override val mention: String
            get() = if (isAnimated) "<a:$name:${id.value}>" else "<:$name:${id.value}>"

        override fun toString() = "Custom(id=$id, name=$name, isAnimated=$isAnimated)"
    }

    data class Unicode(override val name: String) : ReactionEmoji() {
        override val urlFormat: String get() = name
        override val mention: String get() = name
    }

    companion object {
        fun from(guildEmoji: GuildEmoji) = Custom(guildEmoji.id, guildEmoji.name
                ?: error("emojis without name cannot be used to react"), guildEmoji.isAnimated)

        fun from(guildEmoji: RemovedReactionData) = when (guildEmoji.id) {
            null -> Unicode(guildEmoji.name)
            else -> Custom(Snowflake(guildEmoji.id), guildEmoji.name, false)
        }
    }
}

package com.gitlab.kordlib.core.entity

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.MessageBehavior
import com.gitlab.kordlib.core.cache.data.RegionData
import java.util.*

class Region(val data: RegionData, override val kord: Kord) : Entity {
    override val id: Snowflake
        get() = Snowflake(data.id)

    val isCustom: Boolean get() = data.custom

    val isDeprecated: Boolean get() = data.deprecated

    val name: String get() = data.name

    val isOptimal: Boolean get() = data.optimal

    val isVip: Boolean get() = data.vip

    override fun hashCode(): Int = Objects.hash(id)

    override fun equals(other: Any?): Boolean = when(other) {
        is Region -> other.id == id
        else -> false
    }
}

package com.gitlab.kordlib.core.entity

import com.gitlab.kordlib.common.entity.Permissions
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.RoleBehavior
import com.gitlab.kordlib.core.cache.data.RoleData
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import java.awt.Color
import java.util.*

data class Role(
        val data: RoleData,
        override val kord: Kord,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : RoleBehavior {

    override val id: Snowflake
        get() = Snowflake(data.id)

    override val guildId: Snowflake
        get() = Snowflake(data.guildId)

    val color: Color get() = Color(data.color, true)

    val hoisted: Boolean get() = data.hoisted

    val managed: Boolean get() = data.managed

    val mentionable: Boolean get() = data.mentionable

    val name: String get() = data.name

    val permissions: Permissions get() = data.permissions

    val rawPosition: Int get() = data.position

    override fun compareTo(other: Entity): Int = when (other) {
        is Role -> compareBy<Role> { it.rawPosition }.thenBy { it.guildId }.compare(this, other)
        else -> super.compareTo(other)
    }

    /**
     * Returns a new [Role] with the given [strategy].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>): Role = Role(data, kord, strategy.supply(kord))

    override fun hashCode(): Int = Objects.hash(id, guildId)

    override fun equals(other: Any?): Boolean = when(other) {
        is RoleBehavior -> other.id == id && other.guildId == guildId
        else -> false
    }

}

package com.gitlab.kordlib.core.entity

import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy

/**
 * A class that will defer the requesting of [Entities][Entity] to a [supplier].
 * Copies of this class with a different [supplier] can be made through [withStrategy].
 *
 * Unless stated otherwise, all members that fetch [Entities][Entity] will delegate to the [supplier].
 */
interface Strategizable {

    /**
     * The supplier used to request entities.
     */
    val supplier: EntitySupplier

    /**
     * Returns a copy of this class with a new [supplier] provided by the [strategy].
     */
    fun withStrategy(strategy: EntitySupplyStrategy<*>) : Strategizable

}

package com.gitlab.kordlib.core.entity

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.entity.TeamMembershipState
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.cache.data.TeamData
import com.gitlab.kordlib.core.cache.data.TeamMemberData
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.core.exception.EntityNotFoundException

/**
 * A Discord [developer team](https://discord.com/developers/docs/topics/teams) which can own applications.
 */
class Team(val data: TeamData, override val kord: Kord, override val supplier: EntitySupplier) : Entity, Strategizable {
    /**
     * The unique ID of this team.
     */
    override val id: Snowflake
        get() = Snowflake(data.id)

    /**
     * The hash of this team's icon.
     */
    val icon: String? get() = data.icon

    /**
     * A collection of all members of this team.
     */
    val members: List<TeamMember>
        get() = data.members.map { TeamMember(it, kord) }

    /**
     * The ID of the user that owns the team.
     */
    val ownerUserId: Snowflake
        get() = Snowflake(data.id)

    /**
     * Requests to get the team owner through the [supplier].
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the [User] wasn't present.
    */
    suspend fun getUser(): User = supplier.getUser(ownerUserId)

    /**
     * Requests to get the team owner through the [supplier],
     * returns null if the [User] isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
    */
    suspend fun getUserOrNUll(): User? = supplier.getUserOrNull(ownerUserId)

    override fun withStrategy(strategy: EntitySupplyStrategy<*>): Team = Team(data, kord, strategy.supply(kord))
}

/**
 * A member of a Discord developer team.
 */
class TeamMember(val data: TeamMemberData, val kord: Kord) {
    /**
     * An enumeration representing the membership state of this user.
     */
    val membershipState: TeamMembershipState get() = data.membershipState

    /**
     * A collection of permissions granted to this member.
     * At the moment, this collection will only have one element: `*`, meaning the member has all permissions.
     * This is because right now there are no other permissions. Read mode [here](https://discord.com/developers/docs/topics/teams#data-models-team-members-object)
     */
    val permissions: List<String> get() = data.permissions

    /**
     * The unique ID that this member belongs to.
     */
    val teamId: Long get() = data.teamId

    /**
     * The ID of the user this member represents.
     */
    val userId: Snowflake get() = Snowflake(data.userId)

    /**
     * Utility method that gets the user from Kord.
     */
    suspend fun getUser() = kord.getUser(userId)
}

package com.gitlab.kordlib.core.entity

import com.gitlab.kordlib.common.entity.Premium
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.entity.UserFlags
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.KordObject
import com.gitlab.kordlib.core.behavior.UserBehavior
import com.gitlab.kordlib.core.cache.data.UserData
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.rest.Image
import java.util.*

/**
 * An instance of a [Discord User](https://discord.com/developers/docs/resources/user#user-object).
 */
open class User(
        val data: UserData,
        override val kord: Kord, override val supplier: EntitySupplier = kord.defaultSupplier
) : UserBehavior {

    override val id: Snowflake
        get() = Snowflake(data.id)

    val avatar: Avatar get() = Avatar(data, kord)

    /**
     * The username of this user.
     */
    val username: String get() = data.username

    /**
     * The 4-digit code at the end of the user's discord tag.
     */
    val discriminator: String get() = data.discriminator

    /**
     * The flags on a user's account, if present.
     */
    val flags: UserFlags? get() = data.flags

    /**
     * The type of Nitro subscription on a user's account, if present.
     */
    val premiumType: Premium? get() = data.premium

    /**
     * The complete user tag.
     */
    val tag: String get() = "$username#$discriminator"

    /**
     * Whether this user is a bot, if present.
     */
    val isBot: Boolean? get() = data.bot

    override suspend fun asUser(): User = this

    override fun hashCode(): Int = Objects.hash(id)

    override fun equals(other: Any?): Boolean = when (other) {
        is UserBehavior -> other.id == id
        else -> false
    }

    /**
     * Returns a new [User] with the given [strategy].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>): User = User(data, kord, strategy.supply(kord))

    data class Avatar(val data: UserData, override val kord: Kord) : KordObject {

        /**
         * The default avatar url for this user. Discord uses this for users who don't have a custom avatar set.
         */
        val defaultUrl: String get() = "https://cdn.discordapp.com/embed/avatars/${data.discriminator.toInt() % 5}.png"

        /**
         * Whether the user has set their avatar.
         */
        val isCustom: Boolean get() = data.avatar != null

        /**
         * Whether the user has an animated avatar.
         */
        val isAnimated: Boolean get() = data.avatar?.startsWith("a_") ?: false

        /**
         * Gets the avatar url in a supported format, prioritizing [Image.Format.GIF] for animated avatars and [Image.Format.PNG] for others.
         */
        val url: String
            get() = when {
                isAnimated -> getUrl(Image.Format.GIF)
                else -> getUrl(Image.Format.PNG)
            } ?: defaultUrl

        /**
         * Gets the avatar url in a supported [Image.Format], or returns null if the [format] is not supported.
         */
        fun getUrl(format: Image.Format): String? {
            val hash = data.avatar ?: return defaultUrl
            if (!isAnimated && format == Image.Format.GIF) return null

            return "https://cdn.discordapp.com/avatars/${data.id}/$hash.${format.extension}"
        }

        /**
         * Requests to get the [defaultUrl] as an [Image].
         */
        suspend fun getDefaultImage(): Image = Image.fromUrl(kord.resources.httpClient, defaultUrl)

        /**
         * Requests to get the avatar of the user as an [Image], or returns null if the format is not supported.
         *
         * @param format the requested image format, defaults to the behavior of [url] if null.
         */
        suspend fun getImage(format: Image.Format): Image? {
            val url = getUrl(format) ?: return null

            return Image.fromUrl(kord.resources.httpClient, url)
        }

        /**
         * Requests to get the avatar of the user as an [Image], prioritizing gif for animated avatars and png for others.
         */
        suspend fun getImage(): Image = Image.fromUrl(kord.resources.httpClient, url)
    }

}

package com.gitlab.kordlib.core.entity

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.KordObject
import com.gitlab.kordlib.core.cache.data.VoiceStateData
import com.gitlab.kordlib.core.entity.channel.VoiceChannel
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.core.supplier.getChannelOfOrNull
import com.gitlab.kordlib.core.toSnowflakeOrNull

class VoiceState(
        val data: VoiceStateData,
        override val kord: Kord,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : KordObject, Strategizable {

    val guildId: Snowflake? get() = data.guildId.toSnowflakeOrNull()

    val channelId: Snowflake? get() = data.channelId.toSnowflakeOrNull()

    val userId: Snowflake get() = Snowflake(data.userId)

    val sessionId: String get() = data.sessionId

    val isDeafened: Boolean get() = data.deaf

    val isMuted: Boolean get() = data.mute

    val isSelfDeafened: Boolean get() = data.selfDeaf

    val isSelfMuted: Boolean get() = data.selfMute

    val isSuppressed: Boolean get() = data.suppress

    /**
     * Whether this user is streaming using "Go Live".
     */
    val isSelfSteaming: Boolean get() = data.selfStream

    /**
     * Requests to get the voice channel of this voice state.
     * Returns null if the [VoiceChannel] isn't present, or [channelId] is null.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getChannel(): VoiceChannel? = channelId?.let { supplier.getChannelOfOrNull(it) }

    /**
     * Requests to get the guild of this voice state.
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the [Guild] wasn't present.
     */
    suspend fun getGuild(): Guild = supplier.getGuild(guildId!!)

    /**
     * Requests to get the guild of this voice state,
     * returns null if the [Guild] isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getGuildOrNull(): Guild? = guildId?.let { supplier.getGuildOrNull(it) }

    /**
     * Requests to get the member that belongs to this voice state.
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the [Member] wasn't present.
     */
    suspend fun getMember(): Member = supplier.getMember(guildId!!, userId)

    /**
     * Requests to get the member that belongs to this voice state,
     * returns null if the [Member] isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getMemberOrNull(): Member? = guildId?.let { supplier.getMemberOrNull(it, userId) }

    /**
     * Returns a new [VoiceState] with the given [strategy].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>): VoiceState = VoiceState(data, kord, strategy.supply(kord))

}

package com.gitlab.kordlib.core.entity

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.entity.WebhookType
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.GuildBehavior
import com.gitlab.kordlib.core.behavior.WebhookBehavior
import com.gitlab.kordlib.core.behavior.channel.GuildMessageChannelBehavior
import com.gitlab.kordlib.core.cache.data.WebhookData
import com.gitlab.kordlib.core.entity.channel.GuildMessageChannel
import com.gitlab.kordlib.core.exception.EntityNotFoundException
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.core.supplier.getChannelOf
import com.gitlab.kordlib.core.supplier.getChannelOfOrNull
import java.util.*

data class Webhook(
        val data: WebhookData,
        override val kord: Kord,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : WebhookBehavior, Strategizable {

    override val id: Snowflake get() = Snowflake(data.id)

    val type: WebhookType get() = data.type

    val creatorId: Snowflake get() = Snowflake(data.userid)

    val channelId: Snowflake get() = Snowflake(data.channelId)

    val guildId: Snowflake get() = Snowflake(data.guildId)

    val name: String? get() = data.name

    val token: String? get() = data.token

    val channel: GuildMessageChannelBehavior get() = GuildMessageChannelBehavior(guildId, channelId, kord)

    val guild: GuildBehavior get() = GuildBehavior(guildId, kord)

    /**
     * Requests to get the guild this webhook belongs to.
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the [Guild] wasn't present.
     */
    suspend fun getGuild(): Guild = supplier.getGuild(guildId)

    /**
     * Requests to get the guild this webhook belongs to,
     * returns null if the [Guild] isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getGuildOrNull(): Guild? = supplier.getGuildOrNull(guildId)

    /**
     * Requests to get the channel this webhook operates in.
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the [GuildMessageChannel] wasn't present.
     */
    suspend fun getChannel(): GuildMessageChannel = supplier.getChannelOf(channelId)

    /**
     * Requests to get the channel this webhook operates in,
     * returns null if the [GuildMessageChannel] isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getChannelOrNull(): GuildMessageChannel? = supplier.getChannelOfOrNull(channelId)

    /**
     * Returns a new [Webhook] with the given [strategy].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>): Webhook =
            Webhook(data, kord, strategy.supply(kord))

    override fun hashCode(): Int = Objects.hash(id)

    override fun equals(other: Any?): Boolean = when(other) {
        is WebhookBehavior -> other.id == id
        else -> false
    }

}

package com.gitlab.kordlib.core.event

import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.gateway.Gateway
import kotlinx.coroutines.CoroutineScope
import kotlin.coroutines.CoroutineContext

interface Event : CoroutineScope {
    override val coroutineContext: CoroutineContext
        get() = kord.coroutineContext

    /**
     * The Gateway that spawned this event.
     */
    val gateway: Gateway get() = kord.gateway.gateways.getValue(shard)

    val kord: Kord

    /**
     * The shard number of the [gateway] that spawned this event.
     */
    val shard: Int

}

package com.gitlab.kordlib.core.event

import com.gitlab.kordlib.common.entity.DiscordPresenceUser
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.GuildBehavior
import com.gitlab.kordlib.core.behavior.MemberBehavior
import com.gitlab.kordlib.core.entity.*
import com.gitlab.kordlib.core.exception.EntityNotFoundException
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy

class PresenceUpdateEvent(
        val oldUser: User?,
        val user: DiscordPresenceUser,
        val guildId: Snowflake,
        val old: Presence?,
        val presence: Presence,
        override val shard: Int,
        override val supplier: EntitySupplier = presence.kord.defaultSupplier
) : Event, Strategizable {
    override val kord: Kord get() = presence.kord

    /**
     * The behavior of the member whose presence was updated.
     */
    val member: MemberBehavior get() = MemberBehavior(id = Snowflake(user.id), guildId = guildId, kord = kord)

    /**
     * The behavior of the guild in which the presence was updated.
     */
    val guild: GuildBehavior get() = GuildBehavior(guildId, kord)

    /**
     * Requests to get the user whose presence was updated.
     *
     * @throws [RequestException] if something went wrong during the request.
     * @throws [EntityNotFoundException] if the user isn't present.
     */
    suspend fun getUser(): User = supplier.getUser(Snowflake(user.id))

    /**
     * Requests to get the user whose presence was updated, returns null if the user isn't present.
     *
     * @throws [RequestException] if something went wrong during the request.
     */
    suspend fun getUserOrNull(): User? = supplier.getUserOrNull(Snowflake(user.id))

    /**
     * Requests to get the member whose presence was updated.
     *
     * @throws [RequestException] if something went wrong during the request.
     * @throws [EntityNotFoundException] if the member isn't present.
     */
    suspend fun getMember(): Member = supplier.getMember(guildId = guildId, userId = Snowflake(user.id))

    /**
     * Requests to get the member whose presence was updated, returns null if the member isn't present.
     *
     * @throws [RequestException] if something went wrong during the request.
     */
    suspend fun getMemberOrNull(): Member? = supplier.getMemberOrNull(guildId = guildId, userId = Snowflake(user.id))

    /**
     * Requests to get the guild in which the presence was updated.
     *
     * @throws [RequestException] if something went wrong during the request.
     * @throws [EntityNotFoundException] if the guild isn't present.
     */
    suspend fun getGuild(): Guild = supplier.getGuild(guildId)

    /**
     * Requests to get the guild in which the presence was updated, returns null if the guild isn't present.
     *
     * @throws [RequestException] if something went wrong during the request.
     */
    suspend fun getGuildOrNull(): Guild? = supplier.getGuildOrNull(guildId)

    override fun withStrategy(strategy: EntitySupplyStrategy<*>): PresenceUpdateEvent =
            PresenceUpdateEvent(oldUser, user, guildId, old, presence, shard, strategy.supply(kord))
}

package com.gitlab.kordlib.core.event

import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.entity.User

class UserUpdateEvent(
        val old: User?,
        val user: User,
        override val shard: Int
) : Event {
    override val kord: Kord get() = user.kord
}

package com.gitlab.kordlib.core.event

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.GuildBehavior
import com.gitlab.kordlib.core.entity.Guild
import com.gitlab.kordlib.core.entity.Strategizable
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy

class VoiceServerUpdateEvent(
        val token: String,
        val guildId: Snowflake,
        val endpoint: String,
        override val kord: Kord,
        override val shard: Int,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : Event, Strategizable {

    val guild: GuildBehavior get() = GuildBehavior(guildId, kord)

    suspend fun getGuild(): Guild = supplier.getGuild(guildId)

    suspend fun getGuildOrNull():Guild? = supplier.getGuildOrNull(guildId)

    override fun withStrategy(strategy: EntitySupplyStrategy<*>): VoiceServerUpdateEvent =
            VoiceServerUpdateEvent(token, guildId, endpoint, kord, shard, strategy.supply(kord))
}

package com.gitlab.kordlib.core.event

import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.entity.VoiceState

class VoiceStateUpdateEvent(
        val old: VoiceState?,
        val state: VoiceState,
        override val shard: Int
) : Event {
    override val kord: Kord get() = state.kord
}

package com.gitlab.kordlib.core.event

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.GuildBehavior
import com.gitlab.kordlib.core.behavior.channel.GuildMessageChannelBehavior
import com.gitlab.kordlib.core.entity.Guild
import com.gitlab.kordlib.core.entity.Strategizable
import com.gitlab.kordlib.core.entity.channel.GuildMessageChannel
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.core.supplier.getChannelOf
import com.gitlab.kordlib.core.supplier.getChannelOfOrNull

class WebhookUpdateEvent(
        val guildId: Snowflake,
        val channelId: Snowflake,
        override val kord: Kord,
        override val shard: Int,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : Event, Strategizable {

    val channel: GuildMessageChannelBehavior get() = GuildMessageChannelBehavior(guildId, channelId, kord)

    val guild: GuildBehavior get() = GuildBehavior(guildId, kord)

    suspend fun getChannel(): GuildMessageChannel = supplier.getChannelOf(channelId)

    suspend fun getChannelOrNull(): GuildMessageChannel? = supplier.getChannelOfOrNull(channelId)

    suspend fun getGuild(): Guild = guild.asGuild()

    suspend fun getGuildOrNull(): Guild? = guild.asGuildOrNull()

    override fun withStrategy(strategy: EntitySupplyStrategy<*>): WebhookUpdateEvent =
            WebhookUpdateEvent(guildId, channelId, kord, shard, strategy.supply(kord))

}

package com.gitlab.kordlib.core.exception

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.entity.channel.Channel

class EntityNotFoundException : Exception {

    constructor(message: String) : super(message)
    constructor(cause: Throwable) : super(cause)
    constructor(message: String, cause: Throwable) : super(message, cause)

    @Suppress("NOTHING_TO_INLINE")
    companion object {

        @PublishedApi
        internal inline fun entityNotFound(entityType: String, id: Snowflake): Nothing =
                throw EntityNotFoundException("$entityType with id ${id.value} was not found.")

        @PublishedApi
        internal inline fun guildEntityNotFound(entityType: String, guildId: Snowflake, id: Snowflake): Nothing =
                throw EntityNotFoundException("$entityType with id ${id.value} in guild ${guildId.value} was not found.")

        inline fun guildNotFound(guildId: Snowflake): Nothing =
                entityNotFound("Guild", guildId)

        inline fun <reified T : Channel> channelNotFound(channelId: Snowflake): Nothing =
                entityNotFound(T::class.simpleName!!, channelId)

        inline fun memberNotFound(guildId: Snowflake, userId: Snowflake): Nothing =
                guildEntityNotFound("Member", guildId = guildId, id = userId)

        inline fun messageNotFound(channelId: Snowflake, messageId: Snowflake): Nothing = throw EntityNotFoundException("Message with id ${messageId.value} in channel ${channelId.value} was not found")

        inline fun userNotFound(userId: Snowflake): Nothing =
                entityNotFound("User", userId)

        inline fun selfNotFound(): Nothing =
                throw EntityNotFoundException("Self user not found")

        fun roleNotFound(guildId: Snowflake, roleId: Snowflake): Nothing =
                guildEntityNotFound("Role", guildId = guildId, id = roleId)

        fun banNotFound(guildId: Snowflake, userId: Snowflake): Nothing =
                guildEntityNotFound("Ban", guildId = guildId, id = userId)

        fun emojiNotFound(guildId: Snowflake, emojiId: Snowflake): Nothing =
                guildEntityNotFound("GuildEmoji", guildId = guildId, id = emojiId)

        fun webhookNotFound(webhookId: Snowflake): Nothing =
                entityNotFound("Webhook", webhookId)

        fun inviteNotFound(code: String): Nothing =
                throw EntityNotFoundException("Invite with code $code was not found.")

    }

}

package com.gitlab.kordlib.core.exception

class KordInitializationException : Exception {
    constructor(message: String) : super(message)
    constructor(cause: Throwable) : super(cause)
    constructor(message: String, cause: Throwable) : super(message, cause)
}

package com.gitlab.kordlib.core.gateway

import com.gitlab.kordlib.gateway.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.flatMapMerge
import kotlinx.coroutines.flow.map
import kotlin.time.milliseconds

data class ShardEvent(val event: Event, val gateway: Gateway, val shard: Int)

class MasterGateway(
        val gateways: Map<Int, Gateway>
) {

    val averagePing get() = gateways.values.asSequence().map { it.ping.inMilliseconds }.average().milliseconds

    val events: Flow<ShardEvent> = gateways.entries.asFlow()
            .flatMapMerge(gateways.size) { (shard, gateway) -> gateway.events.map { event -> ShardEvent(event, gateway, shard) } }

    suspend fun start(configuration: GatewayConfiguration) = gateways.entries.forEach { (shard, gateway) ->
        val config = configuration.copy(shard = configuration.shard.copy(index = shard))
        gateway.start(config)
    }

    suspend inline fun start(token: String, config: GatewayConfigurationBuilder.() -> Unit = {}) {
        val builder = GatewayConfigurationBuilder(token)
        builder.apply(config)
        start(builder.build())
    }

    suspend fun sendAll(command: Command) = gateways.values.forEach { it.send(command) }

    suspend fun detachAll() = gateways.values.forEach { it.detach() }

    suspend fun stopAll() = gateways.values.forEach { it.stop() }

}

package com.gitlab.kordlib.core.live

import com.gitlab.kordlib.common.annotation.KordPreview
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.entity.Entity
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.event.message.MessageUpdateEvent
import com.gitlab.kordlib.core.event.message.ReactionAddEvent
import com.gitlab.kordlib.core.kordLogger
import kotlinx.atomicfu.atomic
import kotlinx.atomicfu.update
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

/**
 * A Discord entity that only emits events *related* to this entity.
 *
 * For example, a [LiveMessage] will only emit [MessageUpdateEvents][MessageUpdateEvent] of that message, and only emit
 * [reactions][ReactionAddEvent] to that message.
 */
@KordPreview
interface LiveEntity : Entity {
    val events: Flow<Event>

    fun shutDown()
}

@KordPreview
abstract class AbstractLiveEntity : LiveEntity {
    private val mutex = Mutex()
    private val running = atomic(true)

    @Suppress("EXPERIMENTAL_API_USAGE")
    override val events: Flow<Event>
        get() = kord.events
                .takeWhile { running.value }
                .filter { filter(it) }
                .onEach { mutex.withLock { update(it) } }

    protected abstract fun filter(event: Event): Boolean
    protected abstract fun update(event: Event)
    override fun shutDown() = running.update { false }

}

/**
 * Convenience method that will invoke the [consumer] on every event [T], the consumer is launched in the given [scope]
 * or [Kord] by default and will not propagate any exceptions.
 */
@KordPreview
inline fun <reified T : Event> LiveEntity.on(scope: CoroutineScope = kord, noinline consumer: suspend (T) -> Unit) =
        events.buffer(Channel.UNLIMITED).filterIsInstance<T>().onEach {
            runCatching { consumer(it) }.onFailure { kordLogger.catching(it) }
        }.catch { kordLogger.catching(it) }.launchIn(scope)

package com.gitlab.kordlib.core.live

import com.gitlab.kordlib.common.annotation.KordPreview
import com.gitlab.kordlib.core.entity.Entity
import com.gitlab.kordlib.core.entity.Guild
import com.gitlab.kordlib.core.event.*
import com.gitlab.kordlib.core.event.channel.ChannelCreateEvent
import com.gitlab.kordlib.core.event.channel.ChannelDeleteEvent
import com.gitlab.kordlib.core.event.channel.ChannelUpdateEvent
import com.gitlab.kordlib.core.event.guild.*
import com.gitlab.kordlib.core.event.message.*
import com.gitlab.kordlib.core.event.role.RoleCreateEvent
import com.gitlab.kordlib.core.event.role.RoleDeleteEvent
import com.gitlab.kordlib.core.event.role.RoleUpdateEvent

@KordPreview
fun Guild.live(): LiveGuild = LiveGuild(this)

@KordPreview
class LiveGuild(guild: Guild) : AbstractLiveEntity(), Entity by guild {

    var guild: Guild = guild
        private set

    override fun filter(event: Event): Boolean = when (event) {
        is EmojisUpdateEvent -> event.guildId == guild.id

        is IntegrationsUpdateEvent -> event.guildId == guild.id

        is BanRemoveEvent -> event.guildId == guild.id

        is PresenceUpdateEvent -> event.guildId == guild.id

        is UserUpdateEvent -> guild.data.members.any { it.userId == event.user.id.longValue }

        is VoiceServerUpdateEvent -> event.guildId == guild.id
        is VoiceStateUpdateEvent -> event.state.guildId == guild.id

        is WebhookUpdateEvent -> event.guildId == guild.id

        is RoleCreateEvent -> event.guildId == guild.id
        is RoleUpdateEvent -> event.guildId == guild.id
        is RoleDeleteEvent -> event.guildId == guild.id

        is MemberJoinEvent -> event.guildId == guild.id
        is MemberUpdateEvent -> event.guildId == guild.id
        is MemberLeaveEvent -> event.guildId == guild.id

        is ReactionAddEvent -> event.guildId == guild.id
        is ReactionRemoveEvent -> event.guildId == guild.id
        is ReactionRemoveAllEvent -> event.guildId == guild.id

        is MessageCreateEvent -> event.guildId == guild.id
        is MessageUpdateEvent -> event.new.guildId == guild.id.value
        is MessageDeleteEvent -> event.guildId == guild.id

        is ChannelCreateEvent -> event.channel.data.guildId == guild.id.longValue
        is ChannelUpdateEvent -> event.channel.data.guildId == guild.id.longValue
        is ChannelDeleteEvent -> event.channel.data.guildId == guild.id.longValue

        is GuildCreateEvent -> event.guild.id == guild.id
        is GuildUpdateEvent -> event.guild.id == guild.id
        is GuildDeleteEvent -> event.guildId == guild.id

        else -> false
    }

    override fun update(event: Event): Unit = when (event) {
        is EmojisUpdateEvent -> guild = Guild(guild.data.copy(emojis = event.emojis.map { it.data }), kord)

        is RoleCreateEvent -> guild = Guild(guild.data.copy(
                roles = guild.data.roles + event.guildId.longValue
        ), kord)

        is RoleDeleteEvent -> guild = Guild(guild.data.copy(
                roles = guild.data.roles - event.guildId.longValue
        ), kord)

        is PresenceUpdateEvent -> guild = Guild(guild.data.copy(
                presences = guild.data.presences.map {
                    when (it.userId) {
                        event.user.id.toLong() -> event.presence.data
                        else -> it
                    }
                }
        ), kord)

        is VoiceStateUpdateEvent -> guild = Guild(guild.data.copy(
                voiceStates = guild.data.voiceStates.filter { it.userId == event.state.userId.longValue } + event.state.data
        ), kord)

        is MemberJoinEvent -> guild = Guild(guild.data.copy(
                memberCount = guild.data.memberCount?.inc(),
                members = guild.data.members + event.member.memberData
        ), kord)

        is MemberUpdateEvent -> guild = Guild(guild.data.copy(
                members = guild.data.members.map {
                    when (it.userId) {
                        event.memberId.longValue -> it.copy(nick = event.currentNickName, roles = event.currentRoleIds.map { it.value })
                        else -> it
                    }
                }
        ), kord)

        is MemberLeaveEvent -> guild = Guild(guild.data.copy(
                memberCount = guild.data.memberCount?.dec(),
                members = guild.data.members.filter { it.userId != event.guildId.longValue }
        ), kord)

        is ChannelCreateEvent -> guild = Guild(guild.data.copy(
                channels = guild.data.channels + event.channel.id.longValue
        ), kord)

        is ChannelDeleteEvent -> guild = Guild(guild.data.copy(
                channels = guild.data.channels - event.channel.id.longValue
        ), kord)

        is GuildUpdateEvent -> guild = event.guild
        is GuildDeleteEvent -> shutDown()
        else -> Unit
    }

}

package com.gitlab.kordlib.core.live

import com.gitlab.kordlib.common.annotation.KordPreview
import com.gitlab.kordlib.core.entity.Entity
import com.gitlab.kordlib.core.entity.Member
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.event.guild.BanAddEvent
import com.gitlab.kordlib.core.event.guild.GuildDeleteEvent
import com.gitlab.kordlib.core.event.guild.MemberLeaveEvent
import com.gitlab.kordlib.core.event.guild.MemberUpdateEvent

@KordPreview
fun Member.live() = LiveMember(this)

@KordPreview
class LiveMember(member: Member) : AbstractLiveEntity(), Entity by member {
    var member = member
        private set

    override fun filter(event: Event) = when (event) {
        is MemberLeaveEvent -> member.id == event.user.id
        is MemberUpdateEvent -> member.id == event.memberId
        is BanAddEvent -> member.id == event.user.id
        is GuildDeleteEvent -> member.guildId == event.guildId
        else -> false

    }

    override fun update(event: Event) = when (event) {
        is MemberLeaveEvent -> shutDown()
        is BanAddEvent -> shutDown()
        is GuildDeleteEvent -> shutDown()
        is MemberUpdateEvent -> member = Member(member.memberData.copy(
                nick = event.currentNickName,
                roles = event.currentRoleIds.map { it.value }), member.data, kord)

        else -> Unit
    }
}

package com.gitlab.kordlib.core.live

import com.gitlab.kordlib.common.annotation.KordPreview
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.cache.data.ReactionData
import com.gitlab.kordlib.core.entity.Entity
import com.gitlab.kordlib.core.entity.Message
import com.gitlab.kordlib.core.entity.ReactionEmoji
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.event.channel.ChannelDeleteEvent
import com.gitlab.kordlib.core.event.guild.GuildDeleteEvent
import com.gitlab.kordlib.core.event.message.*
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import kotlinx.coroutines.flow.Flow

@KordPreview
suspend fun Message.live() = LiveMessage(this, withStrategy(EntitySupplyStrategy.cacheWithRestFallback).getGuildOrNull()?.id)

@KordPreview
class LiveMessage(message: Message, val guildId: Snowflake?) : AbstractLiveEntity(), Entity by message {

    var message: Message = message
        private set

    override fun filter(event: Event): Boolean = when (event) {
        is ReactionAddEvent -> event.messageId == message.id
        is ReactionRemoveEvent -> event.messageId == message.id
        is ReactionRemoveAllEvent -> event.messageId == message.id

        is MessageCreateEvent -> event.message.id == message.id
        is MessageUpdateEvent -> event.messageId == message.id
        is MessageDeleteEvent -> event.messageId == message.id

        is ChannelDeleteEvent -> event.channel.id == message.channelId

        is GuildDeleteEvent -> event.guildId == guildId
        else -> false
    }

    override fun update(event: Event): Unit = when (event) {
        is ReactionAddEvent -> process(event)
        is ReactionRemoveEvent -> process(event)
        is ReactionRemoveAllEvent -> message = Message(message.data.copy(reactions = emptyList()), kord)

        is MessageUpdateEvent -> message = Message(message.data + event.new, kord)
        is MessageDeleteEvent -> shutDown()

        is ChannelDeleteEvent -> shutDown()

        is GuildDeleteEvent -> shutDown()
        else -> Unit
    }

    private fun process(event: ReactionAddEvent) = with(event.emoji) {
        val animated = this is ReactionEmoji.Custom && isAnimated

        val present = message.data.reactions
                ?.firstOrNull { it.emojiName == name && it.emojiId == id.longValue }

        val reactions = when (present) {
            null -> message.data.reactions.orEmpty() + ReactionData(1, event.userId == kord.selfId, id.longValue, name, animated)
            else -> {
                val updated = present.copy(count = present.count + 1)
                message.data.reactions.orEmpty() - present + updated
            }
        }

        message = Message(message.data.copy(reactions = reactions), kord)
    }

    private fun process(event: ReactionRemoveEvent) = with(event.emoji) {
        val present = message.data.reactions
                ?.firstOrNull { it.emojiName == name && it.emojiId == id.longValue }

        val reactions = when (present) {
            null -> message.data.reactions
            else -> {
                val updated = present.copy(count = present.count - 1)
                message.data.reactions.orEmpty() - present + updated
            }
        }

        message = Message(message.data.copy(reactions = reactions), kord)
    }

}

package com.gitlab.kordlib.core.live

import com.gitlab.kordlib.common.annotation.KordPreview
import com.gitlab.kordlib.core.entity.Entity
import com.gitlab.kordlib.core.entity.Role
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.event.guild.GuildDeleteEvent
import com.gitlab.kordlib.core.event.role.RoleDeleteEvent
import com.gitlab.kordlib.core.event.role.RoleUpdateEvent

@KordPreview
fun Role.live() = LiveRole(this)

@KordPreview
class LiveRole(role: Role) : AbstractLiveEntity(), Entity by role {
    var role = role
        private set

    override fun filter(event: Event) = when (event) {
        is RoleDeleteEvent -> role.id == event.roleId
        is RoleUpdateEvent -> role.id == event.role.id
        is GuildDeleteEvent -> role.guildId == event.guildId
        else -> false
    }

    override fun update(event: Event) = when (event) {
        is RoleDeleteEvent -> shutDown()
        is GuildDeleteEvent -> shutDown()
        is RoleUpdateEvent -> role = event.role
        else -> Unit
    }

}

package com.gitlab.kordlib.core.live

import com.gitlab.kordlib.common.annotation.KordPreview
import com.gitlab.kordlib.core.entity.Entity
import com.gitlab.kordlib.core.entity.User
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.event.UserUpdateEvent

@KordPreview
fun User.live() = LiveUser(this)

@KordPreview
class LiveUser(user: User) : AbstractLiveEntity(), Entity by user {

    var user: User = user
        private set

    override fun filter(event: Event) = when (event) {
        is UserUpdateEvent -> user.id == event.user.id
        else -> false
    }

    override fun update(event: Event) = when (event) {
        is UserUpdateEvent -> user = event.user
        else -> Unit
    }

}

package com.gitlab.kordlib.core.supplier

import com.gitlab.kordlib.cache.api.DataCache
import com.gitlab.kordlib.cache.api.query
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.any
import com.gitlab.kordlib.core.cache.data.*
import com.gitlab.kordlib.core.entity.*
import com.gitlab.kordlib.core.entity.channel.Channel
import com.gitlab.kordlib.core.entity.channel.GuildChannel
import com.gitlab.kordlib.core.exception.EntityNotFoundException
import com.gitlab.kordlib.gateway.Gateway
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.*

/**
 * [EntitySupplier] that uses a [DataCache] to resolve entities.
 *
 * Getting existing entities should not throw any [RequestException] unless
 * specified by the configured cache.
 *
 * The supplier might not be able to return entities independent of their actual
 * existence, their presence depends on incoming events from the [Gateway] and
 * how the cache is set up to store entities.
 *
 * Returned flows without entities will not throw an [EntityNotFoundException]
 * if none are presented like other `getX` functions. Instead, the flow will be empty.
 */
class CacheEntitySupplier(private val kord: Kord) : EntitySupplier {
    /**
     *
     * The Cache this [CacheEntitySupplier] operates on.
     *
     * short-hand for [Kord.cache]
     *
     */
    private val cache: DataCache = kord.cache

    /**
     *  Returns a [Flow] of [Channel]s fetched from cache.
     *
     *  The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    val channels: Flow<Channel>
        get() = cache.query<ChannelData>().asFlow().map { Channel.from(it, kord) }

    /**
     *  fetches all cached [Guild]s
     *
     *  The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    override val guilds: Flow<Guild>
        get() = cache.query<GuildData>().asFlow().map { Guild(it, kord) }

    /**
     *  fetches all cached [Region]s
     *
     *  The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    override val regions: Flow<Region>
        get() = cache.query<RegionData>().asFlow().map { Region(it, kord) }

    /**
     *  fetches all cached [Role]s
     */
    val roles: Flow<Role>
        get() = cache.query<RoleData>().asFlow().map { Role(it, kord) }

    /**
     *  fetches all cached [User]s
     */
    val users: Flow<User>
        get() = cache.query<UserData>().asFlow().map { User(it, kord) }

    /**
     *  fetches all cached [Member]s
     */
    @OptIn(FlowPreview::class)
    val members: Flow<Member>
        get() = cache.query<MemberData>().asFlow().mapNotNull {
            val userData = cache.query<UserData> { UserData::id eq it.userId }.singleOrNull() ?: return@mapNotNull null
            Member(it, userData, kord)
        }

    suspend fun getRole(id: Snowflake): Role? {
        val data = cache.query<RoleData> { RoleData::id eq id.longValue }.singleOrNull() ?: return null

        return Role(data, kord)
    }

    override suspend fun getChannelOrNull(id: Snowflake): Channel? {
        val data = cache.query<ChannelData> { ChannelData::id eq id.longValue }.singleOrNull() ?: return null
        return Channel.from(data, kord)
    }

    override fun getGuildChannels(guildId: Snowflake): Flow<GuildChannel> = cache.query<ChannelData> {
        ChannelData::guildId eq guildId.longValue
    }.asFlow().map { Channel.from(it, kord) as GuildChannel }

    override fun getChannelPins(channelId: Snowflake): Flow<Message> = cache.query<MessageData> {
        MessageData::channelId eq channelId.longValue
        MessageData::pinned eq true
    }.asFlow().map { Message(it, kord) }

    override suspend fun getGuildOrNull(id: Snowflake): Guild? {
        val data = cache.query<GuildData> { GuildData::id eq id.longValue }.singleOrNull() ?: return null
        return Guild(data, kord)
    }

    override suspend fun getMemberOrNull(guildId: Snowflake, userId: Snowflake): Member? {
        val userData = cache.query<UserData> { UserData::id eq userId.longValue }.singleOrNull() ?: return null

        val memberData = cache.query<MemberData> {
            MemberData::userId eq userId.longValue
            MemberData::guildId eq guildId.longValue
        }.singleOrNull() ?: return null

        return Member(memberData, userData, kord)
    }

    override suspend fun getMessageOrNull(channelId: Snowflake, messageId: Snowflake): Message? {
        val data = cache.query<MessageData> { MessageData::id eq messageId.longValue }.singleOrNull()
                ?: return null

        return Message(data, kord)
    }

    override fun getMessagesAfter(messageId: Snowflake, channelId: Snowflake, limit: Int): Flow<Message> {
        require(limit > 0) { "At least 1 item should be requested, but got $limit." }
        return cache.query<MessageData> {
            MessageData::channelId eq channelId.longValue
            MessageData::id gt messageId.longValue
        }.asFlow().map { Message(it, kord) }.take(limit)
    }

    override fun getMessagesBefore(messageId: Snowflake, channelId: Snowflake, limit: Int): Flow<Message> {
        require(limit > 0) { "At least 1 item should be requested, but got $limit." }
        return cache.query<MessageData> {
            MessageData::channelId eq channelId.longValue
            MessageData::id lt messageId.longValue
        }.asFlow().map { Message(it, kord) }.take(limit)
    }

    override fun getMessagesAround(messageId: Snowflake, channelId: Snowflake, limit: Int): Flow<Message> {
        require(limit in 1..100) { "Expected limit to be in 1..100, but was $limit" }
        return flow {
            emitAll(getMessagesBefore(messageId, channelId, limit / 2))
            emitAll(getMessagesAfter(messageId, channelId, limit / 2))
        }
    }

    override suspend fun getSelfOrNull(): User? = getUserOrNull(kord.selfId)

    override suspend fun getRoleOrNull(guildId: Snowflake, roleId: Snowflake): Role? {
        val data = cache.query<RoleData> {
            RoleData::id eq roleId.longValue
            RoleData::guildId eq guildId.longValue
        }.singleOrNull() ?: return null

        return Role(data, kord)
    }

    override fun getGuildRoles(guildId: Snowflake): Flow<Role> = cache.query<RoleData> {
        RoleData::guildId eq guildId.longValue
    }.asFlow().map { Role(it, kord) }

    override suspend fun getGuildBanOrNull(guildId: Snowflake, userId: Snowflake): Ban? {
        val data = cache.query<BanData> {
            BanData::userId eq userId.longValue
            BanData::guildId eq guildId.longValue
        }.singleOrNull() ?: return null
        return Ban(data, kord)
    }

    override fun getGuildBans(guildId: Snowflake): Flow<Ban> = cache.query<BanData> {
        BanData::guildId eq guildId.longValue
    }.asFlow().map { Ban(it, kord) }

    override fun getGuildMembers(guildId: Snowflake, limit: Int): Flow<Member> {
        require(limit > 0) { "At least 1 item should be requested, but got $limit." }
        return cache.query<MemberData> { MemberData::guildId eq guildId.longValue }.asFlow().mapNotNull {
            val userData = cache.query<UserData> { UserData::id eq it.userId }.singleOrNull() ?: return@mapNotNull null
            Member(it, userData, kord)
        }
    }

    override fun getGuildVoiceRegions(guildId: Snowflake): Flow<Region> = cache.query<RegionData> {
        RegionData::guildId eq guildId.longValue
    }.asFlow().map { Region(it, kord) }

    override suspend fun getEmojiOrNull(guildId: Snowflake, emojiId: Snowflake): GuildEmoji? {
        val data = cache.query<EmojiData> {
            EmojiData::guildId eq guildId.longValue
            EmojiData::id eq emojiId.longValue
        }.singleOrNull() ?: return null

        return GuildEmoji(data, kord)
    }

    override fun getEmojis(guildId: Snowflake): Flow<GuildEmoji> = cache.query<EmojiData> {
        EmojiData::guildId eq guildId.longValue
    }.asFlow().map { GuildEmoji(it, kord) }

    override fun getCurrentUserGuilds(limit: Int): Flow<Guild> {
        require(limit > 0) { "At least 1 item should be requested, but got $limit." }
        return guilds.filter {
            members.any { it.id == kord.selfId }
        }.take(limit)
    }

    override fun getChannelWebhooks(channelId: Snowflake): Flow<Webhook> = cache.query<WebhookData> {
        WebhookData::channelId eq channelId.longValue
    }.asFlow().map { Webhook(it, kord) }

    override fun getGuildWebhooks(guildId: Snowflake): Flow<Webhook> = cache.query<WebhookData> {
        WebhookData::guildId eq guildId.longValue
    }.asFlow().map { Webhook(it, kord) }

    override suspend fun getWebhookOrNull(id: Snowflake): Webhook? {
        val data = cache.query<WebhookData> {
            WebhookData::id eq id.longValue
        }.singleOrNull() ?: return null

        return Webhook(data, kord)
    }

    override suspend fun getWebhookWithTokenOrNull(id: Snowflake, token: String): Webhook? {
        val data = cache.query<WebhookData> {
            WebhookData::id eq id.longValue
            WebhookData::token eq token
        }.singleOrNull() ?: return null

        return Webhook(data, kord)
    }

    override suspend fun getUserOrNull(id: Snowflake): User? {
        val data = cache.query<UserData> { UserData::id eq id.longValue }.singleOrNull() ?: return null

        return User(data, kord)
    }

}

package com.gitlab.kordlib.core.supplier

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.entity.*
import com.gitlab.kordlib.core.entity.channel.Channel
import com.gitlab.kordlib.core.entity.channel.GuildChannel
import com.gitlab.kordlib.core.entity.channel.MessageChannel
import com.gitlab.kordlib.core.exception.EntityNotFoundException
import kotlinx.coroutines.flow.Flow

/**
 * An abstraction that allows for requesting Discord entities.
 *
 * @see RestEntitySupplier
 * @see CacheEntitySupplier
 */
interface EntitySupplier {

    /**
     * Requests all [guilds][Guild] this bot is known to be part of.
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    val guilds: Flow<Guild>

    /**
     * Requests all [regions][Region] known to this bot.
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    val regions: Flow<Region>

    /**
     * Requests the [Guild] with the given [id], returns `null` when the guild isn't present.
     *
     * @throws RequestException if something went wrong while retrieving the guild.
     */
    suspend fun getGuildOrNull(id: Snowflake): Guild?

    /**
     * Requests the [Guild] with the given [id].
     *
     * @throws RequestException if something went wrong while retrieving the guild.
     * @throws EntityNotFoundException if the guild is null.
     */
    suspend fun getGuild(id: Snowflake): Guild = getGuildOrNull(id)!!

    /**
     * Requests the [Channel] with the given [id], returns `null` when the channel isn't present.
     *
     * @throws RequestException if something went wrong while retrieving the channel.
     */
    suspend fun getChannelOrNull(id: Snowflake): Channel?

    /**
     * Requests the [Channel] with the given [id].
     *
     * @throws RequestException if something went wrong while retrieving the channel.
     * @throws EntityNotFoundException if the channel is null.
     */
    suspend fun getChannel(id: Snowflake): Channel = getChannelOrNull(id)!!

    /**
     * Requests the [channels][GuildChannel] of the [Guild] with the given [guildId].
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    fun getGuildChannels(guildId: Snowflake): Flow<GuildChannel>

    /**
     * Requests the pinned [messages][Message] of the [Channel] with the given [channelId].
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    fun getChannelPins(channelId: Snowflake): Flow<Message>

    /**
     * Requests the [Member] with the given [userId] in the [Guild] wit the given [guildId],
     * returns `null` when the member isn't present.
     *
     * @throws RequestException if something went wrong while retrieving the member.
     */
    suspend fun getMemberOrNull(guildId: Snowflake, userId: Snowflake): Member?

    /**
     * Requests the [Member] with the given [userId] in the [Guild] wit the given [guildId].
     *
     * @throws RequestException if something went wrong while retrieving the member.
     * @throws EntityNotFoundException if the member was null.
     */
    suspend fun getMember(guildId: Snowflake, userId: Snowflake): Member =
            getMemberOrNull(guildId, userId)
                    ?: EntityNotFoundException.memberNotFound(guildId = guildId, userId = userId)

    /**
     * Requests the [Message] with the given [messageId] in the [MessageChannel] with the given [channelId],
     * returns `null` when the message isn't present.
     *
     * @throws RequestException if something went wrong while retrieving the message.
     */
    suspend fun getMessageOrNull(channelId: Snowflake, messageId: Snowflake): Message?

    /**
     * Requests the [Message] with the given [messageId] in the [MessageChannel] with the given [channelId].
     *
     * @throws RequestException if something went wrong while retrieving the message.
     * @throws EntityNotFoundException if the message is null.
     */
    suspend fun getMessage(channelId: Snowflake, messageId: Snowflake): Message =
            getMessageOrNull(channelId, messageId) ?: EntityNotFoundException.messageNotFound(channelId, messageId)

    /**
     * Requests a flow of messages created after the [Message] with the [messageId]
     * in the [channel][MessageChannel] with the [channelId].
     *
     * The flow may use paginated requests to supply messages, [limit] will limit the maximum number of messages
     * supplied and may optimize the batch size accordingly. A value of [Int.MAX_VALUE] means no limit.
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     *
     * @throws IllegalArgumentException if a [limit] < 1 was supplied.
     */
    fun getMessagesAfter(messageId: Snowflake, channelId: Snowflake, limit: Int = Int.MAX_VALUE): Flow<Message>

    /**
     * Requests a flow of messages created before the [Message] with the [messageId]
     * in the [channel][MessageChannel] with the [channelId].
     *
     * The flow may use paginated requests to supply messages, [limit] will limit the maximum number of messages
     * supplied and may optimize the batch size accordingly. A value of [Int.MAX_VALUE] means no limit.
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     *
     * @throws IllegalArgumentException if a [limit] < 1 was supplied.
     */
    fun getMessagesBefore(messageId: Snowflake, channelId: Snowflake, limit: Int = Int.MAX_VALUE): Flow<Message>

    /**
     * Requests a flow of messages created around the [Message] with the [messageId]
     * in the [channel][MessageChannel] with the [channelId].
     *
     * Unlike [getMessagesAfter] and [getMessagesBefore], this flow can return **a maximum of 100 messages**.
     * As such, the accepted range of [limit] is reduced to 1..100.
     *
     * Supplied messages will be equally distributed
     * before and after the [messageId]. The remaining message for an odd [limit] is undefined and may appear on either
     * side.
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     *
     * @throws IllegalArgumentException if the [limit] is outside the range of 1..100.
     */
    fun getMessagesAround(messageId: Snowflake, channelId: Snowflake, limit: Int = 100): Flow<Message>

    /**
     * Requests the [User] this bot represents, returns null when the user isn't present.
     *
     * @throws RequestException if something went wrong while retrieving the user.
     */
    suspend fun getSelfOrNull(): User?

    /**
     * Requests the [User] this bot represents.
     *
     * @throws RequestException if something went wrong while retrieving the user.
     * @throws EntityNotFoundException if the user was null.
     */
    suspend fun getSelf(): User = getSelfOrNull() ?: EntityNotFoundException.selfNotFound()

    /**
     * Requests the [User] with the given [id], returns null when the user isn't present.
     *
     * @throws RequestException if something went wrong while retrieving the user.
     */
    suspend fun getUserOrNull(id: Snowflake): User?

    /**
     * Requests the [User] with the given [id].
     *
     * @throws RequestException if something went wrong while retrieving the user.
     * @throws EntityNotFoundException if the user was null.
     */
    suspend fun getUser(id: Snowflake): User = getUserOrNull(id) ?: EntityNotFoundException.userNotFound(id)

    /**
     * Requests the [Role] with the given [roleId] in the [Guild] wit the given [guildId],
     * returns null when the role isn't present.
     *
     * @throws RequestException if something went wrong while retrieving the role.
     */
    suspend fun getRoleOrNull(guildId: Snowflake, roleId: Snowflake): Role?

    /**
     * Requests the [Role] with the given [roleId] in the [Guild] wit the given [guildId].
     *
     * @throws RequestException if something went wrong while retrieving the role.
     * @throws EntityNotFoundException if the role was null.
     */
    suspend fun getRole(guildId: Snowflake, roleId: Snowflake): Role =
            getRoleOrNull(guildId, roleId) ?: EntityNotFoundException.roleNotFound(guildId, roleId)

    /**
     * Requests the [roles][Role] of the [Guild] with the given [guildId].
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    fun getGuildRoles(guildId: Snowflake): Flow<Role>

    /**
     * Requests the [Ban] for the user the given [userId] in the [Guild] wit the given [guildId],
     * returns null when the ban isn't present.
     *
     * @throws RequestException if something went wrong while retrieving the ban.
     */
    suspend fun getGuildBanOrNull(guildId: Snowflake, userId: Snowflake): Ban?

    /**
     * Requests the [Ban] for the user the given [userId] in the [Guild] wit the given [guildId],
     * returns null when the ban isn't present.
     *
     * @throws RequestException if something went wrong while retrieving the ban.
     * @throws EntityNotFoundException if the ban was null.
     */
    suspend fun getGuildBan(guildId: Snowflake, userId: Snowflake): Ban = getGuildBanOrNull(guildId, userId)
            ?: EntityNotFoundException.banNotFound(guildId, userId)

    /**
     * Requests the [bans][Ban] of the [Guild] with the given [guildId].
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    fun getGuildBans(guildId: Snowflake): Flow<Ban>

    /**
     * Requests the [members][Member] of the [Guild] with the given [guildId].
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    fun getGuildMembers(guildId: Snowflake, limit: Int = Int.MAX_VALUE): Flow<Member>

    /**
     * Requests the [regions][Region] of the [Guild] with the given [guildId].
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    fun getGuildVoiceRegions(guildId: Snowflake): Flow<Region>

    /**
     * Requests the [GuildEmoji] with the [emojiId] in the [Guild] wit the given [guildId],
     * returns null when the emoji isn't present.
     *
     * @throws RequestException if something went wrong while retrieving the emoji.
     */
    suspend fun getEmojiOrNull(guildId: Snowflake, emojiId: Snowflake): GuildEmoji?

    /**
     * Requests the [GuildEmoji] with the [emojiId] in the [Guild] wit the given [guildId].
     *
     * @throws RequestException if something went wrong while retrieving the emoji.
     * @throws EntityNotFoundException if the emoji was null.
     */
    suspend fun getEmoji(guildId: Snowflake, emojiId: Snowflake): GuildEmoji =
            getEmojiOrNull(guildId, emojiId) ?: EntityNotFoundException.emojiNotFound(guildId, emojiId)

    /**
     * Requests the [guild emojis][GuildEmoji] of the [Guild] with the given [guildId].
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    fun getEmojis(guildId: Snowflake): Flow<GuildEmoji>

    /**
     * Requests the [guild emojis][GuildEmoji] of the [Guild] with the given [guildId].
     *
     *  The flow may use paginated requests to supply guilds, [limit] will limit the maximum number of guilds
     * supplied and may optimize the batch size accordingly. A value of [Int.MAX_VALUE] means no limit.
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     *
     * @throws IllegalArgumentException if a [limit] < 1 was supplied.
     */
    fun getCurrentUserGuilds(limit: Int = Int.MAX_VALUE): Flow<Guild>

    /**
     * Requests the [webhooks][Webhook] of the [MessageChannel] with the given [channelId].
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    fun getChannelWebhooks(channelId: Snowflake): Flow<Webhook>

    /**
     * Requests the [webhooks][Webhook] of the [Guild] with the given [guildId].
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    fun getGuildWebhooks(guildId: Snowflake): Flow<Webhook>

    /**
     * Requests the [Webhook] with the given [id], returns `null` when the webhook isn't present.
     *
     * @throws RequestException if something went wrong while retrieving the webhook.
     */
    suspend fun getWebhookOrNull(id: Snowflake): Webhook?

    /**
     * Requests the [Webhook] with the given [id].
     *
     * @throws RequestException if something went wrong while retrieving the webhook.
     * @throws EntityNotFoundException if the webhook was null.
     */
    suspend fun getWebhook(id: Snowflake): Webhook =
            getWebhookOrNull(id) ?: EntityNotFoundException.webhookNotFound(id)

    /**
     * Requests the [Webhook] with the given [id] using the [token] for authentication,
     * returns null when the webhook isn't present.
     *
     * @throws RequestException if something went wrong while retrieving the webhook.
     */
    suspend fun getWebhookWithTokenOrNull(id: Snowflake, token: String): Webhook?

    /**
     * Requests the [Webhook] with the given [id] using the [token] for authentication.
     *
     * @throws RequestException if something went wrong while retrieving the webhook.
     * @throws EntityNotFoundException if the webhook was null.
     */
    suspend fun getWebhookWithToken(id: Snowflake, token: String): Webhook =
            getWebhookWithTokenOrNull(id, token) ?: EntityNotFoundException.webhookNotFound(id)
}

/**
 * Requests the [Channel] with the given [id] as type [T], returns null if the
 * channel isn't present or if the channel is not of type [T].
 *
 * @throws RequestException if something went wrong while retrieving the channel.
 */
suspend inline fun <reified T : Channel> EntitySupplier.getChannelOfOrNull(id: Snowflake): T? =
        getChannelOrNull(id) as? T

/**
 * Requests the [Channel] with the given [id] as type [T].
 *
 * @throws RequestException if something went wrong while retrieving the channel.
 * @throws EntityNotFoundException if the channel is null.
 * @throws ClassCastException if the returned Channel is not of type [T].
 */
suspend inline fun <reified T : Channel> EntitySupplier.getChannelOf(id: Snowflake): T =
        (getChannelOrNull(id) ?: EntityNotFoundException.channelNotFound<T>(id)) as T

package com.gitlab.kordlib.core.supplier

import com.gitlab.kordlib.core.Kord

/**
 *  A supplier that accepts a [Kord] instance and returns an [EntitySupplier] of type [T].
 */
interface EntitySupplyStrategy<T : EntitySupplier> {

    /**
     * Returns an [EntitySupplier] of type [T] that operates on the [kord] instance.
     */
    fun supply(kord: Kord): T

    companion object {

        /**
         * A supplier providing a strategy which exclusively uses REST calls to fetch entities.
         * See [RestEntitySupplier] for more details.
         */
        val rest = object : EntitySupplyStrategy<RestEntitySupplier> {

            override fun supply(kord: Kord): RestEntitySupplier = RestEntitySupplier(kord)

        }

        /**
         * A supplier providing a strategy which exclusively uses cache to fetch entities.
         * See [CacheEntitySupplier] for more details.
         */
        val cache = object : EntitySupplyStrategy<CacheEntitySupplier> {

            override fun supply(kord: Kord): CacheEntitySupplier = CacheEntitySupplier(kord)

        }

        /**
         * A supplier providing a strategy which will first operate on the [cache] supplier. When an entity
         * is not present from cache it will be fetched from [rest] instead. Operations that return flows
         * will only fall back to rest when the returned flow contained no elements.
         */
        val cacheWithRestFallback = object : EntitySupplyStrategy<EntitySupplier> {

            override fun supply(kord: Kord): EntitySupplier = cache.supply(kord).withFallback(rest.supply(kord))

        }

        /**
         * Create an [EntitySupplyStrategy] from the given [supplier].
         */
        operator fun <T : EntitySupplier> invoke(
                supplier: (Kord) -> T
        ): EntitySupplyStrategy<T> = object : EntitySupplyStrategy<T> {
            override fun supply(kord: Kord): T = supplier(kord)
        }
    }

}

package com.gitlab.kordlib.core.supplier

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.entity.*
import com.gitlab.kordlib.core.entity.channel.Channel
import com.gitlab.kordlib.core.entity.channel.GuildChannel
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy.Companion.cache
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy.Companion.rest
import com.gitlab.kordlib.core.switchIfEmpty
import kotlinx.coroutines.flow.Flow

/**
 * Creates supplier providing a strategy which will first operate on this supplier. When an entity
 * is not present from the first supplier it will be fetched from [other] instead. Operations that return flows
 * will only fall back to [other] when the returned flow contained no elements.
 */
infix fun EntitySupplier.withFallback(other: EntitySupplier): EntitySupplier = FallbackEntitySupplier(this, other)

private class FallbackEntitySupplier(val first: EntitySupplier, val second: EntitySupplier) : EntitySupplier {

    override val guilds: Flow<Guild>
        get() = first.guilds.switchIfEmpty(second.guilds)

    override val regions: Flow<Region>
        get() = first.regions.switchIfEmpty(second.regions)

    override suspend fun getGuildOrNull(id: Snowflake): Guild? =
            first.getGuildOrNull(id) ?: second.getGuildOrNull(id)

    override suspend fun getChannelOrNull(id: Snowflake): Channel? =
            first.getChannelOrNull(id) ?: second.getChannelOrNull(id)

    override fun getGuildChannels(guildId: Snowflake): Flow<GuildChannel> =
            first.getGuildChannels(guildId).switchIfEmpty(second.getGuildChannels(guildId))

    override fun getChannelPins(channelId: Snowflake): Flow<Message> =
            first.getChannelPins(channelId).switchIfEmpty(second.getChannelPins(channelId))

    override suspend fun getMemberOrNull(guildId: Snowflake, userId: Snowflake): Member? =
            first.getMemberOrNull(guildId, userId) ?: second.getMemberOrNull(guildId, userId)

    override suspend fun getMessageOrNull(channelId: Snowflake, messageId: Snowflake): Message? =
            first.getMessageOrNull(channelId, messageId) ?: second.getMessageOrNull(channelId, messageId)

    override suspend fun getMember(guildId: Snowflake, userId: Snowflake): Member =
            getMemberOrNull(guildId, userId)!!

    override fun getMessagesAfter(messageId: Snowflake, channelId: Snowflake, limit: Int): Flow<Message> =
            first.getMessagesAfter(messageId, channelId, limit).switchIfEmpty(second.getMessagesAfter(messageId, channelId, limit))

    override fun getMessagesBefore(messageId: Snowflake, channelId: Snowflake, limit: Int): Flow<Message> =
            first.getMessagesBefore(messageId, channelId, limit).switchIfEmpty(second.getMessagesBefore(messageId, channelId, limit))

    override fun getMessagesAround(messageId: Snowflake, channelId: Snowflake, limit: Int): Flow<Message> =
            first.getMessagesAround(messageId, channelId, limit).switchIfEmpty(second.getMessagesAround(messageId, channelId, limit))

    override suspend fun getSelfOrNull(): User? =
            first.getSelfOrNull() ?: second.getSelfOrNull()

    override suspend fun getUserOrNull(id: Snowflake): User? =
            first.getUserOrNull(id) ?: second.getUserOrNull(id)

    override suspend fun getRoleOrNull(guildId: Snowflake, roleId: Snowflake): Role? =
            first.getRoleOrNull(guildId, roleId) ?: second.getRoleOrNull(guildId, roleId)

    override fun getGuildRoles(guildId: Snowflake): Flow<Role> =
            first.getGuildRoles(guildId).switchIfEmpty(second.getGuildRoles(guildId))

    override suspend fun getGuildBanOrNull(guildId: Snowflake, userId: Snowflake): Ban? =
            first.getGuildBanOrNull(guildId, userId) ?: second.getGuildBanOrNull(guildId, userId)

    override fun getGuildBans(guildId: Snowflake): Flow<Ban> =
            first.getGuildBans(guildId).switchIfEmpty(second.getGuildBans(guildId))

    override fun getGuildMembers(guildId: Snowflake, limit: Int): Flow<Member> =
            first.getGuildMembers(guildId, limit).switchIfEmpty(second.getGuildMembers(guildId, limit))

    override fun getGuildVoiceRegions(guildId: Snowflake): Flow<Region> =
            first.getGuildVoiceRegions(guildId).switchIfEmpty(second.getGuildVoiceRegions(guildId))

    override suspend fun getEmojiOrNull(guildId: Snowflake, emojiId: Snowflake): GuildEmoji? =
            first.getEmojiOrNull(guildId, emojiId) ?: second.getEmojiOrNull(guildId, emojiId)

    override fun getEmojis(guildId: Snowflake): Flow<GuildEmoji> =
            first.getEmojis(guildId).switchIfEmpty(second.getEmojis(guildId))

    override fun getCurrentUserGuilds(limit: Int): Flow<Guild> =
            first.getCurrentUserGuilds(limit).switchIfEmpty(second.getCurrentUserGuilds(limit))

    override fun getChannelWebhooks(channelId: Snowflake): Flow<Webhook> =
            first.getChannelWebhooks(channelId).switchIfEmpty(second.getChannelWebhooks(channelId))

    override fun getGuildWebhooks(guildId: Snowflake): Flow<Webhook> =
            first.getGuildWebhooks(guildId).switchIfEmpty(second.getGuildWebhooks(guildId))

    override suspend fun getWebhookOrNull(id: Snowflake): Webhook? =
            first.getWebhookOrNull(id) ?: second.getWebhookOrNull(id)

    override suspend fun getWebhookWithTokenOrNull(id: Snowflake, token: String): Webhook? =
            first.getWebhookWithTokenOrNull(id, token) ?: second.getWebhookWithTokenOrNull(id, token)

}

package com.gitlab.kordlib.core.supplier

import com.gitlab.kordlib.common.entity.DiscordGuildMember
import com.gitlab.kordlib.common.entity.DiscordPartialGuild
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.cache.data.*
import com.gitlab.kordlib.core.catchNotFound
import com.gitlab.kordlib.core.entity.*
import com.gitlab.kordlib.core.entity.channel.Channel
import com.gitlab.kordlib.core.entity.channel.GuildChannel
import com.gitlab.kordlib.core.exception.EntityNotFoundException
import com.gitlab.kordlib.core.paginateBackwards
import com.gitlab.kordlib.core.paginateForwards
import com.gitlab.kordlib.rest.request.RestRequestException
import com.gitlab.kordlib.rest.route.Position
import com.gitlab.kordlib.rest.service.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.take
import kotlin.math.min

/**
 * [EntitySupplier] that uses a [RestClient] to resolve entities.
 *
 * Error codes besides 429(Too Many Requests) will throw a [RestRequestException],
 * 404(Not Found) will be caught by the `xOrNull` variant and return null instead.
 *
 * This supplier will always be able to resolve entities if they exist according
 * to Discord, entities will always be up to date at the moment of the call.
 */
class RestEntitySupplier(val kord: Kord) : EntitySupplier {

    private val auditLog: AuditLogService get() = kord.rest.auditLog
    private val channel: ChannelService get() = kord.rest.channel
    private val emoji: EmojiService get() = kord.rest.emoji
    private val guild: GuildService get() = kord.rest.guild
    private val invite: InviteService get() = kord.rest.invite
    private val user: UserService get() = kord.rest.user
    private val voice: VoiceService get() = kord.rest.voice
    private val webhook: WebhookService get() = kord.rest.webhook
    private val application: ApplicationService get() = kord.rest.application

    override val guilds: Flow<Guild>
        get() = paginateForwards(idSelector = DiscordPartialGuild::id, batchSize = 100) { position -> user.getCurrentUserGuilds(position, 100) }
                .map {
                    val guild = guild.getGuild(it.id)
                    val data = GuildData.from(guild)
                    Guild(data, kord)
                }

    override val regions: Flow<Region>
        get() = flow {
            voice.getVoiceRegions().forEach {
                val data = RegionData.from(null, it)
                emit(Region(data, kord))
            }
        }

    override suspend fun getChannelOrNull(id: Snowflake): Channel? = catchNotFound { Channel.from(channel.getChannel(id.value).toData(), kord) }

    override fun getGuildChannels(guildId: Snowflake): Flow<GuildChannel> = flow {
        for (channelData in guild.getGuildChannels(guildId.value))
            emit(Channel.from(ChannelData.from(channelData), kord) as GuildChannel)
    }

    override fun getChannelPins(channelId: Snowflake): Flow<Message> = flow {
        for (messageData in channel.getChannelPins(channelId.value))
            emit(Message(MessageData.from(messageData), kord))
    }

    override suspend fun getGuildOrNull(id: Snowflake): Guild? = catchNotFound { Guild(guild.getGuild(id.value).toData(), kord) }

    /**
     * Returns the preview of the guild matching the [id]. The bot does not need to present in this guild
     * for this to complete successfully.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     * @throws [EntityNotFoundException] if the preview was not found.
     */
    suspend fun getGuildPreview(id: Snowflake): GuildPreview = getGuildPreviewOrNull(id)
            ?: EntityNotFoundException.entityNotFound("Guild preview", id)

    /**
     * Returns the preview of the guild matching the [id]. The bot does not need to present in this guild
     * for this to complete successfully. Returns null if the preview was not found.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun getGuildPreviewOrNull(id: Snowflake): GuildPreview? = catchNotFound {
        val discordPreview = guild.getGuildPreview(id.value)
        return GuildPreview(GuildPreviewData.from(discordPreview), kord)
    }

    override suspend fun getMemberOrNull(guildId: Snowflake, userId: Snowflake): Member? = catchNotFound {
        val member = guild.getGuildMember(guildId = guildId.value, userId = userId.value)
        val memberData = member.toData(guildId = guildId.value, userId = userId.value)
        val userData = member.user!!.toData()
        return Member(memberData, userData, kord)
    }

    override suspend fun getMessageOrNull(channelId: Snowflake, messageId: Snowflake): Message? = catchNotFound {
        Message(channel.getMessage(channelId = channelId.value, messageId = messageId.value).toData(), kord)
    }

    override fun getMessagesAfter(messageId: Snowflake, channelId: Snowflake, limit: Int): Flow<Message> {
        require(limit > 0) { "At least 1 item should be requested, but got $limit." }
        val batchSize = min(100, limit)

        val flow = paginateForwards(messageId, batchSize, idSelector = { it.id }) { position ->
            kord.rest.channel.getMessages(channelId.value, position, batchSize)
        }.map {
            val data = MessageData.from(it)
            Message(data, kord)
        }

        return if (limit != Int.MAX_VALUE) flow.take(limit) else flow
    }

    override fun getMessagesBefore(messageId: Snowflake, channelId: Snowflake, limit: Int): Flow<Message> {
        require(limit > 0) { "At least 1 item should be requested, but got $limit." }
        val batchSize = min(100, limit)

        val flow = paginateBackwards(messageId, batchSize, idSelector = { it.id }) { position ->
            kord.rest.channel.getMessages(channelId.value, position, batchSize)
        }.map {
            val data = MessageData.from(it)
            Message(data, kord)
        }

        return if (limit != Int.MAX_VALUE) flow.take(limit) else flow
    }

    override fun getMessagesAround(messageId: Snowflake, channelId: Snowflake, limit: Int) = flow {
        val responses = kord.rest.channel.getMessages(channelId.value, Position.Around(messageId.value))

        for (response in responses) {
            val data = MessageData.from(response)
            emit(Message(data, kord))
        }
    }

    override suspend fun getSelfOrNull(): User? = catchNotFound {
        User(user.getCurrentUser().toData(), kord)
    }

    override suspend fun getUserOrNull(id: Snowflake): User? = catchNotFound { User(user.getUser(id.value).toData(), kord) }

    override suspend fun getRoleOrNull(guildId: Snowflake, roleId: Snowflake): Role? = catchNotFound {
        val response = guild.getGuildRoles(guildId.value)
                .firstOrNull { it.id == roleId.value } ?: return@catchNotFound null

        return Role(RoleData.from(guildId.value, response), kord)
    }

    override suspend fun getGuildBanOrNull(guildId: Snowflake, userId: Snowflake) = catchNotFound {
        val response = guild.getGuildBan(guildId.value, userId.value)
        val data = BanData.from(guildId.value, response)
        Ban(data, kord)

    }

    override fun getGuildRoles(guildId: Snowflake): Flow<Role> = flow {
        for (roleData in guild.getGuildRoles(guildId.value))
            emit(Role(RoleData.from(guildId.value, roleData), kord))
    }

    override fun getGuildBans(guildId: Snowflake): Flow<Ban> = flow {
        for (banData in guild.getGuildBans(guildId.value))
            emit(Ban(BanData.from(guildId.value, banData), kord))
    }

    override fun getGuildMembers(guildId: Snowflake, limit: Int): Flow<Member> {
        require(limit > 0) { "At least 1 item should be requested, but got $limit." }
        val batchSize = min(1000, limit)

        val flow = paginateForwards(idSelector = { it.user!!.id }, batchSize = batchSize) { position ->
            kord.rest.guild.getGuildMembers(guildId = guildId.value, position = position, limit = batchSize)
        }.map {
            val userData = it.user!!.toData()
            val memberData = it.toData(guildId = guildId.value, userId = it.user!!.id)
            Member(memberData, userData, kord)
        }

        return if (limit != Int.MAX_VALUE) flow.take(limit)
        else flow
    }

    override fun getGuildVoiceRegions(guildId: Snowflake): Flow<Region> = flow {
        for (region in guild.getGuildVoiceRegions(guildId.value)) {
            val data = RegionData.from(guildId.value, region)
            emit(Region(data, kord))
        }
    }

    fun getReactors(channelId: Snowflake, messageId: Snowflake, emoji: ReactionEmoji): Flow<User> =
            paginateForwards(batchSize = 100, idSelector = { it.id }) { position ->
                kord.rest.channel.getReactions(
                        channelId = channelId.value,
                        messageId = messageId.value,
                        emoji = emoji.urlFormat,
                        limit = 100,
                        position = position
                )
            }.map {
                val data = UserData.from(it)
                User(data, kord)
            }

    override suspend fun getEmojiOrNull(guildId: Snowflake, emojiId: Snowflake) = catchNotFound {
        val data = EmojiData.from(guildId.value, emojiId.value, emoji.getEmoji(guildId.value, emojiId.value))
        GuildEmoji(data, kord)
    }

    override fun getEmojis(guildId: Snowflake) = flow {
        for (emoji in emoji.getEmojis(guildId.value)) {
            val data = EmojiData.from(guildId = guildId.value, id = emoji.id!!, entity = emoji)
            emit(GuildEmoji(data, kord))
        }
    }

    override fun getCurrentUserGuilds(limit: Int): Flow<Guild> {
        require(limit > 0) { "At least 1 item should be requested, but got $limit." }
        val batchSize = min(100, limit)

        val flow = paginateForwards(batchSize = batchSize, idSelector = { it.id.value }) { position ->
            user.getCurrentUserGuilds(position, batchSize).map { Guild(guild.getGuild(it.id).toData(), kord) }
        }

        return if (limit != Int.MAX_VALUE) flow.take(limit)
        else flow
    }

    override fun getChannelWebhooks(channelId: Snowflake): Flow<Webhook> = flow {
        for (webhook in webhook.getChannelWebhooks(channelId.value)) {
            val data = WebhookData.from(webhook)
            emit(Webhook(data, kord))
        }
    }

    override fun getGuildWebhooks(guildId: Snowflake): Flow<Webhook> = flow {
        for (webhook in webhook.getGuildWebhooks(guildId.value)) {
            val data = WebhookData.from(webhook)
            emit(Webhook(data, kord))
        }
    }

    override suspend fun getWebhookOrNull(id: Snowflake): Webhook? = catchNotFound {
        val data = WebhookData.from(webhook.getWebhook(id.value))
        return Webhook(data, kord)
    }

    override suspend fun getWebhookWithTokenOrNull(id: Snowflake, token: String): Webhook? = catchNotFound {
        val data = WebhookData.from(webhook.getWebhookWithToken(id.value, token))
        return Webhook(data, kord)
    }

    suspend fun getInviteOrNull(code: String, withCounts: Boolean): Invite? = catchNotFound {
        val response = invite.getInvite(code, withCounts)
        return Invite(InviteData.from(response), kord)
    }

    suspend fun getInvite(code: String, withCounts: Boolean = true): Invite =
            getInviteOrNull(code, withCounts) ?: EntityNotFoundException.inviteNotFound(code)

    /**
     * Requests to get the information of the current application.
     *
     * Entities will be fetched from Discord directly, ignoring any cached values.
     * @throws RestRequestException when the request failed.
     */
    suspend fun getApplicationInfo(): ApplicationInfo {
        val response = application.getCurrentApplicationInfo()
        return ApplicationInfo(ApplicationInfoData.from(response), kord)
    }

}

package com.gitlab.kordlib.gateway.builder

import com.gitlab.kordlib.common.annotation.KordDsl
import com.gitlab.kordlib.common.entity.DiscordActivity
import com.gitlab.kordlib.common.entity.ActivityType
import com.gitlab.kordlib.common.entity.Status
import com.gitlab.kordlib.gateway.Presence
import com.gitlab.kordlib.gateway.UpdateStatus
import java.time.Instant

@KordDsl
class PresenceBuilder  {
    private var game: DiscordActivity? = null
    var status: Status = Status.Online
    var afk: Boolean = false
    var since: Instant? = null

    fun playing(name: String) {
        game = DiscordActivity(name, ActivityType.Game)
    }

    fun listening(name: String) {
        game = DiscordActivity(name, ActivityType.Listening)
    }

    fun streaming(name: String, url: String) {
        game = DiscordActivity(name, ActivityType.Streaming, url = url)
    }

    fun watching(name: String) {
        game = DiscordActivity(name, ActivityType.Watching)
    }

    fun toUpdateStatus(): UpdateStatus = UpdateStatus(since?.toEpochMilli(), game, status, afk)

    fun toPresence(): Presence = Presence(status, afk, since?.toEpochMilli(), game)
}

package com.gitlab.kordlib.gateway.handler

import com.gitlab.kordlib.gateway.Close
import com.gitlab.kordlib.gateway.Event
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import mu.KotlinLogging
import kotlin.coroutines.CoroutineContext

private val logger = KotlinLogging.logger("[Handler]")

internal abstract class Handler(val flow: Flow<Event>, val name: String, private val dispatcher: CoroutineDispatcher = Dispatchers.Default) : CoroutineScope {

    override val coroutineContext: CoroutineContext
        get() = Job() + dispatcher

    init {
        launch {
            start()
        }
    }

    open fun start() {}

    inline fun <reified T> on(crossinline block: suspend (T) -> Unit) {
        flow.filterIsInstance<T>().onEach {
            try {
                block(it)
            } catch (exception: Exception) {
                logger.error(exception) { "[$name]" }
            }
        }.launchIn(this)
    }

}

package com.gitlab.kordlib.gateway.handler

import com.gitlab.kordlib.common.ratelimit.RateLimiter
import com.gitlab.kordlib.common.ratelimit.consume
import com.gitlab.kordlib.gateway.*
import kotlinx.atomicfu.AtomicRef
import kotlinx.atomicfu.atomic
import kotlinx.atomicfu.update
import kotlinx.coroutines.flow.Flow

internal class HandshakeHandler(
        flow: Flow<Event>,
        private val send: suspend (Command) -> Unit,
        private val sequence: Sequence,
        private val identifyRateLimiter: RateLimiter
) : Handler(flow, "HandshakeHandler") {

    lateinit var configuration: GatewayConfiguration

    private val session: AtomicRef<String?> = atomic(null)

    private val identify
        get() = configuration.identify

    private val resume
        get() = Resume(configuration.token, session.value!!, sequence.value)

    private val sessionStart get() = session.value == null

    override fun start() {
        on<Ready> { event ->
            session.update { event.data.sessionId }
        }

        on<Hello> {
            identifyRateLimiter.consume {
                if (sessionStart) send(identify)
                else send(resume)
            }
        }

        on<Close.SessionReset> {
            session.update { null }
        }
    }
}

package com.gitlab.kordlib.gateway.handler

import com.gitlab.kordlib.gateway.*
import kotlinx.atomicfu.atomic
import kotlinx.atomicfu.update
import kotlinx.coroutines.ObsoleteCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlin.time.*

@ObsoleteCoroutinesApi
internal class HeartbeatHandler(
        flow: Flow<Event>,
        private val send: suspend (Command) -> Unit,
        private val restart: suspend () -> Unit,
        private val ping: (Duration) -> Unit,
        private val sequence: Sequence,
        private val ticker: Ticker = Ticker(),
        private val timeSource: TimeSource = TimeSource.Monotonic
) : Handler(flow, "HeartbeatHandler") {

    private val possibleZombie = atomic(false)
    private var timestamp: TimeMark = timeSource.markNow()

    override fun start() {
        on<Event> {
            possibleZombie.update { false }
        }

        on<Hello> { hello ->
            ticker.tickAt(hello.heartbeatInterval) {
                if (possibleZombie.value) {
                    restart()
                } else {
                    possibleZombie.update { true }
                    timestamp = timeSource.markNow()
                    send(Command.Heartbeat(sequence.value))
                }
            }

            timestamp = TimeSource.Monotonic.markNow()
            send(Command.Heartbeat(sequence.value))
        }

        on<Heartbeat> {
            timestamp = TimeSource.Monotonic.markNow()
            send(Command.Heartbeat(sequence.value))
        }

        on<HeartbeatACK> {
            ping(timestamp.elapsedNow())
        }

        on<Close> {
            ticker.stop()
        }
    }
}

package com.gitlab.kordlib.gateway.handler

import com.gitlab.kordlib.gateway.*
import kotlinx.coroutines.flow.Flow

internal class InvalidSessionHandler(
        flow: Flow<Event>,
        private val restart: suspend (event: Close) -> Unit
) : Handler(flow, "InvalidSessionHandler") {

    override fun start() {
        on<InvalidSession> {
            if (it.resumable) restart(Close.Reconnecting)
            else restart(Close.SessionReset)
        }
    }

}

package com.gitlab.kordlib.gateway.handler

import com.gitlab.kordlib.gateway.Event
import com.gitlab.kordlib.gateway.Reconnect
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow

internal class ReconnectHandler(
        flow: Flow<Event>,
        private val reconnect: suspend () -> Unit
) : Handler(flow, "ReconnectHandler") {

    override fun start() {
        on<Reconnect> {
            reconnect()
        }
    }
}

package com.gitlab.kordlib.gateway.handler

import com.gitlab.kordlib.gateway.Close
import com.gitlab.kordlib.gateway.DispatchEvent
import com.gitlab.kordlib.gateway.Event
import com.gitlab.kordlib.gateway.Sequence
import kotlinx.coroutines.flow.Flow

internal class SequenceHandler(
        flow: Flow<Event>,
        private val sequence: Sequence
) : Handler(flow, "SequenceHandler") {

    init {
        on<DispatchEvent> { event ->
            sequence.value = event.sequence ?: sequence.value
        }

        on<Close.SessionReset> {
            sequence.value = null
        }
    }

}

package com.gitlab.kordlib.gateway.retry

import kotlinx.atomicfu.atomic
import kotlinx.atomicfu.update
import kotlinx.coroutines.delay
import mu.KotlinLogging
import kotlin.time.Duration
import kotlin.time.milliseconds

private val linearRetryLogger = KotlinLogging.logger { }

/**
 * A Retry that linearly increases the delay time between a given minimum and maximum over a given amount of tries.
 *
 * @param firstBackoff the initial delay for a [retry] invocation.
 * @param maxBackoff the maximum delay for a [retry] invocation.
 * @param maxTries the maximum amount of consecutive retries before [hasNext] returns false.
 */
class LinearRetry constructor(
        private val firstBackoff: Duration,
        private val maxBackoff: Duration,
        private val maxTries: Int
) : Retry {

    constructor(firstBackoffMillis: Long, maxBackoffMillis: Long, maxTries: Int) :
            this(firstBackoffMillis.milliseconds, maxBackoffMillis.milliseconds, maxTries)

    init {
        require(firstBackoff.isPositive()) { "firstBackoff needs to be positive but was ${firstBackoff.toLongMilliseconds()} ms" }
        require(maxBackoff.isPositive()) { "maxBackoff needs to be positive but was ${maxBackoff.toLongMilliseconds()} ms" }
        require(maxBackoff.minus(firstBackoff).isPositive()) { "maxBackoff ${maxBackoff.toLongMilliseconds()} ms needs to be bigger than firstBackoff ${firstBackoff.toLongMilliseconds()} ms" }
        require(maxTries > 0) { "maxTries needs to be positive but was $maxTries" }
    }

    private val tries = atomic(0)

    override val hasNext: Boolean
        get() = tries.value < maxTries

    override fun reset() {
        tries.update { 0 }
    }

    override suspend fun retry() {
        if (!hasNext) error("max retries exceeded")

        tries.incrementAndGet()
        var diff = (maxBackoff - firstBackoff).toLongMilliseconds() / maxTries
        diff *= tries.value
        linearRetryLogger.trace { "retry attempt ${tries.value}, delaying for $diff ms" }
        delay(diff)
    }

}

package com.gitlab.kordlib.gateway.retry

/**
 * A strategy for retrying after after a failed action.
 */
interface Retry {
    /**
     * Whether this strategy has any more retries left.
     */
    val hasNext: Boolean

    /**
     * Resets the underlying retry counter if this Retry uses an maximum for consecutive [retry] invocations.
     * This should be called after a successful [retry].
     */
    fun reset()

    /**
     * Suspends for a certain amount of time.
     */
    suspend fun retry()
}

package com.gitlab.kordlib.rest.builder

import com.gitlab.kordlib.common.annotation.KordDsl

@KordDsl
interface RequestBuilder<T> {
    fun toRequest() : T
}

@KordDsl
interface AuditRequestBuilder<T> : RequestBuilder<T> {
    /**
     * The reason for this request, this will be displayed in the audit log.
     */
    var reason: String?
}

package com.gitlab.kordlib.rest.json

import kotlinx.serialization.*
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

/**
 * Detailed error codes sent Discord API  in the JSON error response.
 *
 * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
 * */
@Serializable(with = JsonErrorCode.JsonErrorCodeSerializer::class)
enum class JsonErrorCode(val code: Int) {
    /**
     * Undocumented error
     */
    Unknown(-1),
    /**
     * General error (such as a malformed request body, amongst other things).
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    General(0),
    /**
     * Unknown account.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    UnknownAccount(10001),
    /**
     * Unknown application.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    UnknownApplication(10002),
    /**
     * Unknown channel.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    UnknownChannel(10003),
    /**
     * Unknown guild.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    UnknownGuild(10004),
    /**
     * Unknown integration.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    UnknownIntegration(10005),
    /**
     * Unknown invite.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    UnknownInvite(10006),
    /**
     * Unknown member.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    UnknownMember(10007),
    /**
     * Unknown message.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    UnknownMessage(10008),
    /**
     * Unknown permission overwrite.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    UnknownPermissionOverwrite(10009),
    /**
     * Unknown provider.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    UnknownProvider(10010),
    /**
     * Unknown role.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    UnknownRole(10011),
    /**
     * Unknown token.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    UnknownToken(10012),
    /**
     * Unknown user.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    UnknownUser(10013),
    /**
     * Unknown emoji.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    UnknownEmoji(10014),
    /**
     * Unknown webhook.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    UnknownWebhook(10015),

    /**
     * Unknown ban.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    UnknownBan(10026),

    /**
     * Unknown SKU.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    UnknownSKU(10027),
    /**
     * Unknown store Listing.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    UnknownStoreListing(10028),
    /**
     * Unknown entitlement.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    UnknownEntitlement(10029),
    /**
     * Unknown build.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    UnknownBuild(10030),
    /**
     * Unknown lobby.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    UnknownLobby(10031),
    /**
     * Unknown branch.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    UnknownBranch(10032),
    /**
     * Unknown redistributable.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    UnknownRedistributable(10036),
    /**
     * Bots cannot use this endpoint.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    NonBotEndpoint(20001),
    /**
     * Only bots can use this endpoint.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    BotsEndpoint(20002),
    /**
     * Maximum number of guilds reached (100).
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    MaxGuilds(30001),
    /**
     * Maximum number of friends reached (1000).
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    MaxFriends(30002),
    /**
     * Maximum number of pins reached for the channel (50).
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    MaxPins(30003),
    /**
     * Maximum number of guild roles reached (250).
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    MaxGuildRoles(30005),
    /**
     * Maximum number of webhooks reached (10).
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    MaxWebhooks(30007),
    /**
     * Maximum number of reactions reached (20).
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    MaxReactions(30010),
    /**
     * Maximum number of guild channels reached (500).
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    MaxGuildChannels(30013),

    /**
     * Maximum number of attachments in a message reached (10).
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    MaxAttachments(30015),

    /**
     * Maximum number of invites reached (1000).
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    MaxInvites(30016),
    /**
     * Unauthorized. Provide a valid token and try again.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    Unauthorized(40001),

    /**
     * You need to verify your account in order to perform this action.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    VerifyAccount(40002),

    /**
     * Request entity too large. Try sending something smaller in size.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    RequestEntityTooLarge(40005),
    /**
     * This feature has been temporarily disabled server-side.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    TemporarilyDisabled(40006),
    /**
     * The user is banned from this guild.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    UserBannedFromGuild(40007),

    /**
     * Missing access.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    MissingAccess(50001),
    /**
     * Invalid account type.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    InvalidAccountType(50002),
    /**
     * Cannot execute action on a DM channel.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    CannotExecuteOnDM(50003),
    /**
     * Guild widget disabled.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    GuildWidgetDisabled(50004),
    /**
     * Cannot edit a message authored by another user.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    CannotEditMessageByAnotherUser(50005),
    /**
     * Cannot send an empty message.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    CannotSendEmptyMessage(50006),
    /**
     * Cannot send messages to this user.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    CannotSendMessagesToUser(50007),
    /**
     * Cannot send messages in a voice channel.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    CannotSendMessagesInVoiceChannel(50008),
    /**
     * Channel verification level is too high for you to gain access.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    ChannelVerificationTooHigh(50009),
    /**
     * OAuth2 application does not have a bot.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    OAuth2HasNoBot(50010),
    /**
     * OAuth2 application limit reachedOAuth2 application limit reached.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    OAuth2ApplicationLimit(50011),
    /**
     * Invalid OAuth2 state.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    InvalidOAuth2State(50012),
    /**
     * You lack permissions to perform that action.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    PermissionLack(50013),
    /**
     * Invalid authentication token provided.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    InvalidAuthToken(50014),
    /**
     * Provided too few or too many messages to delete. Must provide at least 2 and fewer than 100 messages to delete.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    TooLongNote(50015),
    /**
     * A message can only be pinned to the channel it was sent in.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    ProvidedMessageCountInsufficient(50016),
    /**
     * A message can only be pinned to the channel it was sent in.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    CannotPinMessageFromAnotherChannel(50019),
    /**
     * Invite code was either invalid or taken.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    InviteCodeInvalidOrTaken(50020),
    /**
     * Cannot execute action on a system message.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    CannotExecuteOnSystemMessage(50021),
    /**
     * Invalid OAuth2 access token provided.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    InvalidOAuth2AccessToken(50025),
    /**
     * A message provided was too old to bulk delete.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    BulkDeleteOldMessage(50034),
    /**
     * Invalid form body (returned for both application/json and multipart/form-data bodies),
     * or invalid Content-Type provided.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    InvalidFormBody(50035),
    /**
     * An invite was accepted to a guild the application's bot is not in.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    NonBotGuildInviteAccepted(50036),
    /**
     * Invalid API version provided.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    InvalidAPIVersion(50041),
    /**
     * Reaction was blocked.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    ReactionBlocked(90001),
    /**
     * API resource is currently overloaded. Try again a little later.
     *
     * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes)
     */
    APIResourceOverloaded(130000);

    @Serializer(forClass = JsonErrorCode::class)
    companion object JsonErrorCodeSerializer : KSerializer<JsonErrorCode>  {
        override val descriptor = PrimitiveSerialDescriptor("JsonErrorCodeSerializer", PrimitiveKind.INT)

        override fun deserialize(decoder: Decoder): JsonErrorCode {
            val code = decoder.decodeInt()
            return values().firstOrNull { it.code == code } ?: Unknown
        }

        override fun serialize(encoder: Encoder, value: JsonErrorCode) {
            encoder.encodeInt(value.code)
        }
    }

}

package com.gitlab.kordlib.rest.json

import kotlinx.serialization.*
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

/**
 * This is a very stupid serializer and you should feel ashamed for calling this.
 * Essentially, there's a use case where a [com.gitlab.kordlib.rest.route.Route] may *sometimes* return
 * a value, and sometimes nothing.
 *
 * `nullable` doesn't save you here since it'll expect at least something, and thus throws on an empty input.
 * Thus this crime against control flow was born. This will try to serialize the type as if it wasn't null, and
 * swallow any exceptions while doing so, returning null instead. This is incredibly bad because we won't propagate any
 * actual bugs.
 */
@OptIn(ExperimentalSerializationApi::class)
internal val <T> KSerializer<T>.optional: KSerializer<T?>
    get() = object: KSerializer<T?> {

        override val descriptor: SerialDescriptor
            get() = this@optional.descriptor

        override fun deserialize(decoder: Decoder): T? = try {
            decoder.decodeSerializableValue(this@optional)
        } catch (e :Exception) {
            null
        }

        override fun serialize(encoder: Encoder, value: T?) {
            if (value == null) return encoder.encodeNull()
            else encoder.encodeSerializableValue<T>(this@optional, value)
        }
    }

package com.gitlab.kordlib.rest.ratelimit

import com.gitlab.kordlib.common.ratelimit.BucketRateLimiter
import com.gitlab.kordlib.rest.request.Request
import com.gitlab.kordlib.rest.request.RequestIdentifier
import com.gitlab.kordlib.rest.request.identifier
import kotlinx.atomicfu.atomic
import kotlinx.atomicfu.update
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.delay
import kotlinx.coroutines.sync.Mutex
import mu.KLogger
import java.time.Clock
import java.time.Duration
import java.util.concurrent.ConcurrentHashMap
import kotlin.time.minutes

abstract class AbstractRateLimiter internal constructor(val clock: Clock) : RequestRateLimiter {
    internal abstract val logger: KLogger

    internal val autoBanRateLimiter = BucketRateLimiter(25000, 10.minutes)
    internal val globalSuspensionPoint = atomic(Reset(clock.instant()))
    internal val buckets = ConcurrentHashMap<BucketKey, Bucket>()
    internal val routeBuckets = ConcurrentHashMap<RequestIdentifier, MutableSet<BucketKey>>()

    internal val BucketKey.bucket get() = buckets.getOrPut(this) { Bucket(this) }
    internal val Request<*, *>.buckets get() = routeBuckets[identifier].orEmpty().map { it.bucket }
    internal fun RequestIdentifier.addBucket(id: BucketKey) = routeBuckets.getOrPut(this) { mutableSetOf() }.add(id)

    internal suspend fun Reset.await() {
        val duration = Duration.between(clock.instant(), value)
        if (duration.isNegative) return
        delay(duration.toMillis())
    }

    open override suspend fun await(request: Request<*, *>): RequestToken {
        globalSuspensionPoint.value.await()
        val buckets = request.buckets
        buckets.forEach { it.awaitAndLock() }

        autoBanRateLimiter.consume()
        return newToken(request, buckets)
    }

    internal abstract fun newToken(request: Request<*, *>, buckets: List<Bucket>): RequestToken

    internal abstract inner class AbstractRequestToken(val identity: RequestIdentifier, val requestBuckets: List<Bucket>) : RequestToken {
        private val completableDeferred = CompletableDeferred<Unit>()

        override val completed: Boolean
            get() = completableDeferred.isCompleted

        open override suspend fun complete(response: RequestResponse) {
            response.bucketKey?.let { key ->
                if (identity.addBucket(key)) {
                    logger.trace { "[DISCOVERED]:[BUCKET]:Bucket ${response.bucketKey?.value} discovered for $identity" }
                    buckets[key] = key.bucket
                }
            }

            when (response) {
                is RequestResponse.GlobalRateLimit -> {
                    logger.trace { "[RATE LIMIT]:[GLOBAL]:exhausted until ${response.reset.value}" }
                    globalSuspensionPoint.update { response.reset }
                }
                is RequestResponse.BucketRateLimit -> {
                    logger.trace { "[RATE LIMIT]:[BUCKET]:Bucket ${response.bucketKey.value} was exhausted until ${response.reset.value}" }
                    response.bucketKey.bucket.updateReset(response.reset)
                }
            }

            completableDeferred.complete(Unit)
            requestBuckets.forEach { it.unlock() }
        }

    }

    internal inner class Bucket(val id: BucketKey) {
        val reset = atomic(Reset(clock.instant()))
        val mutex = Mutex()

        suspend fun awaitAndLock() {
            mutex.lock()
            logger.trace { "[BUCKET]:Bucket ${id.value} waiting until ${reset.value}" }
            reset.value.await()
        }

        fun updateReset(newValue: Reset) {
            reset.update { newValue }
        }

        fun unlock() = mutex.unlock()

    }

}

package com.gitlab.kordlib.rest.ratelimit

import com.gitlab.kordlib.rest.request.Request
import com.gitlab.kordlib.rest.request.RequestIdentifier
import com.gitlab.kordlib.rest.request.identifier
import kotlinx.coroutines.delay
import kotlinx.coroutines.sync.Mutex
import mu.KLogger
import mu.KotlinLogging
import java.time.Clock
import java.time.Duration
import java.time.Instant

private val requestLogger = KotlinLogging.logger {}

/**
 * A [RequestRateLimiter] that handles all [requests][Request] in sequential order,
 * minimizing the possibility of rate limits. Requests are handled in call order
 * and will suspend to adhere to global and bucket rate limits.
 *
 * @param clock a [Clock] used for calculating suspension times, present for testing purposes.
 */
class ExclusionRequestRateLimiter(clock: Clock = Clock.systemUTC()) : AbstractRateLimiter(clock) {

    override val logger: KLogger get() = requestLogger
    private val sequentialLock = Mutex()

    override suspend fun await(request: Request<*, *>): RequestToken {
        sequentialLock.lock()
        return super.await(request)
    }

    override fun newToken(request: Request<*, *>, buckets: List<Bucket>): RequestToken {
        return ExclusionRequestToken(request.identifier, buckets)
    }

    private inner class ExclusionRequestToken(identity: RequestIdentifier, requestBuckets: List<Bucket>) : AbstractRequestToken(identity, requestBuckets) {

        override suspend fun complete(response: RequestResponse) {
            super.complete(response)
            sequentialLock.unlock()
        }

    }

}

package com.gitlab.kordlib.rest.ratelimit

import com.gitlab.kordlib.common.annotation.KordUnsafe
import com.gitlab.kordlib.rest.request.Request
import com.gitlab.kordlib.rest.request.RequestIdentifier
import com.gitlab.kordlib.rest.request.identifier
import mu.KLogger
import mu.KotlinLogging
import java.time.Clock

private val parallelLogger = KotlinLogging.logger {}

/**
 * A [RequestRateLimiter] that tries to handle [requests][Request] in a parallel order.
 * Requests are run sequentially per bucket,
 * allowing requests that do not share a common rate limit to run uninterrupted.
 * Requests that share a common bucket are handled in call order
 * and will suspend to adhere to global and bucket rate limits.
 *
 * Using this [RequestRateLimiter] increases the chance of a exceeding the global rate limit, which in exceedingly
 * unlikely cases might result in your bot's account getting temporarily banned.
 * As such, the [ExclusionRequestRateLimiter] is generally preferred.
 *
 * @param clock a [Clock] used for calculating suspension times, present for testing purposes.
 */
@KordUnsafe
class ParallelRequestRateLimiter(clock: Clock = Clock.systemUTC()) : AbstractRateLimiter(clock) {

    override val logger: KLogger
        get() = parallelLogger

    override fun newToken(request: Request<*, *>, buckets: List<Bucket>): RequestToken = ParallelRequestToken(request.identifier, buckets)

    private inner class ParallelRequestToken(identity: RequestIdentifier, requestBuckets: List<Bucket>) : AbstractRequestToken(identity, requestBuckets)

}

package com.gitlab.kordlib.rest.ratelimit

import com.gitlab.kordlib.rest.request.Request
import java.lang.Exception
import java.time.Instant
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

/**
 * A rate limiter that follows [Discord's rate limits](https://discord.com/developers/docs/topics/rate-limits) for
 * the REST api.
 */
interface RequestRateLimiter {

    /**
     * Awaits all active rate limits for the [request], returning a [RequestToken] used to process the result of the
     * [request].
     */
    suspend fun await(request: Request<*, *>): RequestToken

}

/**
 * [Awaits][RequestRateLimiter.await] the rate limits for the [request] and then runs [consumer].
 * Throws an [IllegalStateException] if the supplied [RequestToken] was not completed.
 */
@OptIn(ExperimentalContracts::class)
suspend inline fun <T> RequestRateLimiter.consume(
        request: Request<*, *>,
        consumer: (token: RequestToken) -> T
): T {
    contract {
        callsInPlace(consumer, InvocationKind.EXACTLY_ONCE)
    }
    val token = await(request)
    try {
        val result = consumer(token)
        check(token.completed) {
            token.complete(RequestResponse.Error)
            "token was not completed"
        }
        return result
    } catch (exception: Throwable) {
        if (!token.completed) token.complete(RequestResponse.Error)
        throw exception
    }

}

data class RateLimit(val total: Total, val remaining: Remaining) {
    val isExhausted: Boolean get() = remaining.value == 0L

    companion object
}

inline class Total(val value: Long) {
    companion object
}

inline class Remaining(val value: Long) {
    companion object
}

/**
 * The unique identifier of this bucket.
 */
inline class BucketKey(val value: String) {
    companion object
}

/**
 * The [instant][value] when the current bucket gets reset.
 */
inline class Reset(val value: Instant) {
    companion object
}

sealed class RequestResponse {
    abstract val bucketKey: BucketKey?
    abstract val rateLimit: RateLimit?
    abstract val reset: Reset?

    /**
     * The request returned a non-rate limit error code.
     */
    object Error : RequestResponse() {
        override val bucketKey: BucketKey? = null
        override val rateLimit: RateLimit? = null
        override val reset: Reset? = null
    }

    /**
     * The request returned a response without errors.
     */
    data class Accepted(override val bucketKey: BucketKey?, override val rateLimit: RateLimit?, override val reset: Reset) : RequestResponse()

    /**
     * The request returned a global rate limit error.
     */
    data class GlobalRateLimit(override val bucketKey: BucketKey?, override val rateLimit: RateLimit?, override val reset: Reset) : RequestResponse()

    /**
     * The request returned a bucket rate limit error.
     */
    data class BucketRateLimit(override val bucketKey: BucketKey, override val rateLimit: RateLimit?, override val reset: Reset) : RequestResponse()

    companion object

}

/**
 * A completable token linked to a [Request].
 */
interface RequestToken {

    /**
     * Whether [complete] has been called.
     */
    val completed: Boolean

    /**
     * Completes the [Request], updating the rate limit data from the [response].
     */
    suspend fun complete(response: RequestResponse)
}

@file:Suppress("unused")

package com.gitlab.kordlib.rest.request

import com.gitlab.kordlib.rest.ratelimit.BucketKey
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.readBytes
import io.ktor.client.statement.readText
import java.time.Clock
import java.time.Duration
import java.time.Instant

private const val rateLimitGlobalHeader = "X-RateLimit-Global"
private const val retryAfterHeader = "Retry-After"
private const val rateLimitRemainingHeader = "X-RateLimit-Remaining"
private const val resetTimeHeader = "X-RateLimit-Reset"
private const val bucketRateLimitKey = "X-RateLimit-Bucket"
private const val rateLimit = "X-RateLimit-Limit"
private const val rateLimitResetAfter = "X-RateLimit-Reset-After"

val HttpResponse.channelResetPoint: Instant
    get() {
        val unixSeconds = headers[resetTimeHeader]?.toDouble() ?: return Instant.now()
        return Instant.ofEpochMilli(unixSeconds.times(1000).toLong())
    }

fun HttpResponse.channelResetPoint(clock: Clock) : Instant {
    val seconds = headers[rateLimitResetAfter]?.toDouble() ?: return clock.instant()
    return clock.instant().plus(Duration.ofMillis(seconds.times(1000).toLong()))
}

val HttpResponse.isRateLimit get() = status.value == 429
val HttpResponse.isError get() = status.value in 400 until 600
val HttpResponse.isErrorWithRateLimit get() = status.value == 403 || status.value == 401
val HttpResponse.isGlobalRateLimit get() = headers[rateLimitGlobalHeader] != null
val HttpResponse.rateLimitTotal get() = headers[rateLimit]?.toLongOrNull()
val HttpResponse.rateLimitRemaining get() = headers[rateLimitRemainingHeader]?.toLongOrNull()
val HttpResponse.isChannelRateLimit get() = headers[rateLimitRemainingHeader]?.toIntOrNull() == 0
val HttpResponse.bucket: BucketKey? get() = headers[bucketRateLimitKey]?.let { BucketKey(it) }

/**
 * The unix time (in ms) when the global rate limit gets reset.
 */
fun HttpResponse.globalSuspensionPoint(clock: Clock): Long {
    val msWait = headers[retryAfterHeader]?.toLong() ?: return 0
    return msWait + clock.millis()
}

fun HttpResponse.logString(body: String) = "[RESPONSE]:${status.value}:${call.request.method.value}:${call.request.url} body:$body"

suspend fun HttpResponse.errorString(): String {
    val message = String(this.readBytes())
    return if (message.isBlank()) "${logString(message)}"
    else "${logString(message)}"
}

fun Request<*,*>.logString(body: String): String {
    val method = route.method.value
    val path = route.path
    val params = routeParams.entries
            .joinToString(",", "[", "]") { (key, value) -> "$key=$value" }

    return "[REQUEST]:$method:$path params:$params body:$body"
}

package com.gitlab.kordlib.rest.request

import com.gitlab.kordlib.rest.ratelimit.*
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.features.defaultRequest
import io.ktor.client.request.forms.MultiPartFormDataContent
import io.ktor.client.request.header
import io.ktor.client.request.request
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.HttpStatement
import io.ktor.client.statement.readText
import io.ktor.content.TextContent
import io.ktor.http.takeFrom
import kotlinx.serialization.json.Json
import kotlinx.serialization.parse
import kotlinx.serialization.stringify
import mu.KotlinLogging
import java.time.Clock

internal val jsonDefault = Json {
    encodeDefaults = false
    allowStructuredMapKeys = true
    ignoreUnknownKeys = true
    isLenient = true
}

/**
 * A [RequestHandler] that uses ktor's [HttpClient][client] to execute requests and accepts a [requestRateLimiter]
 * to schedule requests.
 *
 * @param client A [HttpClient] configured with the required headers for identification.
 * @param clock A [Clock] to calculate bucket reset times, exposed for testing.
 * @param parser Serializer used to parse payloads.
 */
@Suppress("EXPERIMENTAL_API_USAGE")
class KtorRequestHandler(
        private val client: HttpClient,
        private val requestRateLimiter: RequestRateLimiter = ExclusionRequestRateLimiter(),
        private val clock: Clock = Clock.systemUTC(),
        private val parser: Json = jsonDefault
) : RequestHandler {
    private val logger = KotlinLogging.logger("[R]:[KTOR]:[${requestRateLimiter.javaClass.simpleName}]")

    override tailrec suspend fun <B : Any, R> handle(request: Request<B, R>): R {
        val response = requestRateLimiter.consume(request) {
            val httpRequest = client.createRequest(request)
            val response = httpRequest.execute()

            it.complete(RequestResponse.from(response, clock))

            response
        }

        return when {
            response.isRateLimit -> handle(request)
            response.isError -> throw KtorRequestException(response, response.errorString())
            else -> parser.decodeFromString(request.route.strategy, response.readText())
        }
    }

    private suspend fun <B : Any, R> HttpClient.createRequest(request: Request<B, R>) = request<HttpStatement> {
        method = request.route.method
        headers.append("X-RateLimit-Precision", "millisecond")
        headers.appendAll(request.headers)

        url {
            url.takeFrom(com.gitlab.kordlib.rest.route.Route.baseUrl)
            encodedPath += request.path
            parameters.appendAll(request.parameters)
        }

        request.body?.let {
            when (request) {
                is MultipartRequest<*, *> -> {
                    headers.append("payload_json", parser.encodeToString(it.strategy, it.body))
                    this.body = MultiPartFormDataContent(request.data)
                }

                is JsonRequest<*, *> -> {
                    val json = parser.encodeToString(it.strategy, it.body)
                    this.body = TextContent(json, io.ktor.http.ContentType.Application.Json)
                }
            }
        }
    }

    companion object {

        operator fun invoke(
                token: String,
                requestRateLimiter: RequestRateLimiter = ExclusionRequestRateLimiter(),
                clock: Clock = Clock.systemUTC(),
                parser: Json = jsonDefault
        ): KtorRequestHandler {
            val client = HttpClient(CIO) {
                expectSuccess = false
                defaultRequest { header("Authorization", "Bot $token") }
            }
            return KtorRequestHandler(client, requestRateLimiter, clock, parser)
        }
    }

}

fun RequestResponse.Companion.from(response: HttpResponse, clock: Clock): RequestResponse {
    val bucket = response.bucket
    val rateLimit = run {
        val total = Total(response.rateLimitTotal ?: return@run null)
        val remaining = Remaining(response.rateLimitRemaining ?: return@run null)
        RateLimit(total, remaining)
    }

    val reset = Reset(response.channelResetPoint(clock))

    return when {
        response.isGlobalRateLimit -> RequestResponse.GlobalRateLimit(bucket, rateLimit, reset)
        response.isRateLimit -> RequestResponse.BucketRateLimit(bucket
                ?: BucketKey("missing"), rateLimit, reset)
        response.isError -> RequestResponse.Error
        else -> RequestResponse.Accepted(bucket, rateLimit, reset)
    }
}

package com.gitlab.kordlib.rest.request

import com.gitlab.kordlib.rest.route.Route
import io.ktor.client.request.forms.append
import io.ktor.client.request.forms.formData
import io.ktor.http.encodeURLQueryComponent
import io.ktor.util.StringValues
import io.ktor.utils.io.streams.outputStream
import kotlinx.serialization.SerializationStrategy

sealed class Request<B : Any, R> {
    abstract val route: Route<R>
    abstract val routeParams: Map<Route.Key, String>
    abstract val headers: StringValues
    abstract val parameters: StringValues
    abstract val body: RequestBody<B>?
    abstract val files: List<Pair<String, java.io.InputStream>>?

    val path: String
        get() {
            var path = route.path
            routeParams.forEach { (k, v) -> path = path.replaceFirst(k.identifier, v.encodeURLQueryComponent()) }
            return path
        }
}

val Request<*, *>.identifier get() = when { //The major identifier is always the 'biggest' entity.
    Route.GuildId in routeParams -> RequestIdentifier.MajorParamIdentifier(route, routeParams.getValue(Route.GuildId))
    Route.ChannelId in routeParams -> RequestIdentifier.MajorParamIdentifier(route, routeParams.getValue(Route.ChannelId))
    Route.WebhookId in routeParams -> RequestIdentifier.MajorParamIdentifier(route, routeParams.getValue(Route.WebhookId))
    else -> RequestIdentifier.RouteIdentifier(route)
}

/**
 * A ['per-route'](https://discord.com/developers/docs/topics/rate-limits) identifier for rate limiting purposes.
 */
sealed class RequestIdentifier {
    /**
     * An identifier that does not contain any major parameters.
     */
    data class RouteIdentifier(val route: Route<*>) : RequestIdentifier()

    /**
     * An identifier with a major parameter.
     */
    data class MajorParamIdentifier(val route: Route<*>, val param: String) : RequestIdentifier()
}

data class RequestBody<T>(val strategy: SerializationStrategy<T>, val body: T) where T : Any

class JsonRequest<B : Any, R>(
        override val route: Route<R>,
        override val routeParams: Map<Route.Key, String>,
        override val parameters: StringValues,
        override val headers: StringValues,
        override val body: RequestBody<B>?
) : Request<B, R>() {
    override val files: List<Pair<String, java.io.InputStream>>? = null
}

class MultipartRequest<B : Any, R>(
        override val route: Route<R>,
        override val routeParams: Map<Route.Key, String>,
        override val parameters: StringValues,
        override val headers: StringValues,
        override val body: RequestBody<B>?,
        override val files: List<Pair<String, java.io.InputStream>> = emptyList()
) : Request<B, R>() {

    val data = formData {
        if (files.size == 1) append("file", filename = files[0].first) {
            files[0].second.copyTo(outputStream())
        } else files.forEachIndexed { index, pair ->
            val name = pair.first
            val inputStream = pair.second
            append("file$index", name) { inputStream.copyTo(outputStream()) }
        }
    }

}

package com.gitlab.kordlib.rest.request

import com.gitlab.kordlib.rest.route.Route
import io.ktor.http.*
import kotlinx.serialization.SerializationStrategy

class RequestBuilder<T>(private val route: Route<T>, keySize: Int = 2) {

    val keys: MutableMap<Route.Key, String> = HashMap(keySize, 1f)

    private val headers = HeadersBuilder()
    private val parameters = ParametersBuilder()

    private var body: RequestBody<*>? = null
    private val files: MutableList<Pair<String, java.io.InputStream>> = mutableListOf()

    operator fun MutableMap<String, String>.set(key: Route.Key, value: String) = set(key.identifier, value)

    fun <E : Any> body(strategy: SerializationStrategy<E>, body: E) {
        this.body = RequestBody(strategy, body)
    }

    fun parameter(key: String, value: Any) = parameters.append(key, value.toString())

    fun header(key: String, value: String) = headers.append(key, value.encodeURLQueryComponent())

    fun file(name: String, input: java.io.InputStream) {
        files.add(name to input)
    }

    fun file(pair: Pair<String, java.io.InputStream>) {
        files.add(pair)
    }

    fun build(): Request<*, T> = when {
        files.isEmpty() -> JsonRequest(route, keys, parameters.build(), headers.build(), body)
        else -> MultipartRequest(route, keys, parameters.build(), headers.build(), body, files.orEmpty())
    }

}

package com.gitlab.kordlib.rest.request

/**
 * Handles Discord API requests.
 */
interface RequestHandler {

    /**
     * Executes the [request], abiding by the active rate limits and returning the response [R].
     * Throws an [RestRequestException] when a non-rate limit error response is returned.
     */
    @Throws(RestRequestException::class)
    suspend fun <B : Any,R>  handle(request: Request<B,R>): R

}

package com.gitlab.kordlib.rest.request

import com.gitlab.kordlib.common.exception.RequestException
import io.ktor.client.statement.HttpResponse

/**
 * Signals that an error related to interactions with the REST API occurred.
 */
abstract class RestRequestException(val code: Int, message: String) : RequestException(message)

class KtorRequestException(val response: HttpResponse, message: String) : RestRequestException(response.status.value, message)

package com.gitlab.kordlib.rest.route

sealed class Position(val key: String, val value: String) {
    class Before(id: String) : Position("before", id)
    class After(id: String) : Position("after", id)
    class Around(id: String) : Position("around", id)
}

package com.gitlab.kordlib.rest.route

import com.gitlab.kordlib.common.annotation.KordPreview
import com.gitlab.kordlib.common.entity.*
import com.gitlab.kordlib.rest.json.optional
import com.gitlab.kordlib.rest.json.response.*
import io.ktor.http.HttpMethod
import kotlinx.serialization.*
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.StructureKind
import kotlinx.serialization.descriptors.buildSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import com.gitlab.kordlib.common.entity.DiscordEmoji as EmojiEntity

internal const val REST_VERSION_PROPERTY_NAME = "com.gitlab.kordlib.rest.version"
internal val restVersion get() = System.getenv(REST_VERSION_PROPERTY_NAME) ?: "v6"

sealed class Route<T>(
        val method: HttpMethod,
        val path: String,
        val strategy: DeserializationStrategy<T>
) {

    override fun toString(): String = "Route(method:${method.value},path:$path,strategy:${strategy.descriptor.serialName})"

    object GatewayGet
        : Route<GatewayResponse>(HttpMethod.Get, "/gateway", GatewayResponse.serializer())

    object GatewayBotGet
        : Route<BotGatewayResponse>(HttpMethod.Get, "/gateway/bot", BotGatewayResponse.serializer())

    object AuditLogGet
        : Route<AuditLogResponse>(HttpMethod.Get, "/guilds/$GuildId/audit-logs", AuditLogResponse.serializer())

    object ChannelGet
        : Route<DiscordChannel>(HttpMethod.Get, "/channels/$ChannelId", DiscordChannel.serializer())

    object ChannelPut
        : Route<DiscordChannel>(HttpMethod.Put, "/channels/$ChannelId", DiscordChannel.serializer())

    object ChannelPatch
        : Route<DiscordChannel>(HttpMethod.Patch, "/channels/$ChannelId", DiscordChannel.serializer())

    object ChannelDelete
        : Route<DiscordChannel>(HttpMethod.Delete, "/channels/$ChannelId", DiscordChannel.serializer())

    object MessagePost
        : Route<DiscordMessage>(HttpMethod.Post, "/channels/$ChannelId/messages", DiscordMessage.serializer())

    object MessageGet
        : Route<DiscordMessage>(HttpMethod.Get, "/channels/$ChannelId/messages/$MessageId", DiscordMessage.serializer())

    object MessagesGet
        : Route<List<DiscordMessage>>(HttpMethod.Get, "/channels/$ChannelId/messages", ListSerializer(DiscordMessage.serializer()))

    object PinsGet
        : Route<List<DiscordMessage>>(HttpMethod.Get, "/channels/$ChannelId/pins", ListSerializer(DiscordMessage.serializer()))

    object InvitesGet
        : Route<List<InviteResponse>>(HttpMethod.Get, "/channels/$ChannelId/invites", ListSerializer(InviteResponse.serializer()))

    object InvitePost
        : Route<InviteResponse>(HttpMethod.Post, "/channels/$ChannelId/invites", InviteResponse.serializer())

    object ReactionPut
        : Route<Unit>(HttpMethod.Put, "/channels/$ChannelId/messages/$MessageId/reactions/$Emoji/@me", NoStrategy)

    object OwnReactionDelete
        : Route<Unit>(HttpMethod.Delete, "/channels/$ChannelId/messages/$MessageId/reactions/$Emoji/@me", NoStrategy)

    object ReactionDelete
        : Route<Unit>(HttpMethod.Delete, "/channels/$ChannelId/messages/$MessageId/reactions/$Emoji/$UserId", NoStrategy)

    object DeleteAllReactionsForEmoji
        : Route<Unit>(HttpMethod.Delete, "/channels/$ChannelId/messages/$MessageId/reactions/$Emoji", NoStrategy)

    object MessageDelete
        : Route<Unit>(HttpMethod.Delete, "/channels/$ChannelId/messages/$MessageId", NoStrategy)

    object BulkMessageDeletePost
        : Route<Unit>(HttpMethod.Post, "/channels/$ChannelId/messages/bulk-delete", NoStrategy)

    object PinDelete
        : Route<Unit>(HttpMethod.Delete, "/channels/$ChannelId/pins/$MessageId", NoStrategy)

    object PinPut
        : Route<Unit>(HttpMethod.Put, "/channels/$ChannelId/pins/$MessageId", NoStrategy)

    object AllReactionsDelete
        : Route<Unit>(HttpMethod.Delete, "/channels/$ChannelId/messages/$MessageId/reactions", NoStrategy)

    object ChannelPermissionDelete
        : Route<Unit>(HttpMethod.Delete, "/channels/$ChannelId/permissions/$OverwriteId", NoStrategy)

    object ChannelPermissionPut
        : Route<Unit>(HttpMethod.Put, "/channels/$ChannelId/permissions/$OverwriteId", NoStrategy)

    object ReactionsGet
        : Route<List<DiscordUser>>(HttpMethod.Get, "/channels/$ChannelId/messages/$MessageId/reactions/$Emoji", ListSerializer(DiscordUser.serializer()))

    object TypingIndicatorPost
        : Route<Unit>(HttpMethod.Post, "/channels/$ChannelId/typing", NoStrategy)

    object GroupDMUserDelete
        : Route<Unit>(HttpMethod.Delete, "/channels/$ChannelId/recipients/$UserId", NoStrategy)

    object GroupDMUserPut
        : Route<Unit>(HttpMethod.Put, "/channels/$ChannelId/recipients/$UserId", NoStrategy)

    object EditMessagePatch
        : Route<DiscordMessage>(HttpMethod.Patch, "/channels/$ChannelId/messages/$MessageId", DiscordMessage.serializer())

    object GuildEmojiGet
        : Route<EmojiEntity>(HttpMethod.Get, "/guilds/$GuildId/emojis/$EmojiId", EmojiEntity.serializer())

    object GuildEmojisGet
        : Route<List<EmojiEntity>>(HttpMethod.Get, "/guilds/$GuildId/emojis", ListSerializer(EmojiEntity.serializer()))

    object GuildEmojiDelete
        : Route<Unit>(HttpMethod.Delete, "/guilds/$GuildId/emojis/$EmojiId", NoStrategy)

    object GuildEmojiPost
        : Route<EmojiEntity>(HttpMethod.Post, "/guilds/$GuildId/emojis", EmojiEntity.serializer())

    object GuildEmojiPatch
        : Route<EmojiEntity>(HttpMethod.Patch, "/guilds/$GuildId/emojis/$EmojiId", EmojiEntity.serializer())

    object InviteGet
        : Route<InviteResponse>(HttpMethod.Get, "/invites/$InviteCode", InviteResponse.serializer())

    object InviteDelete
        : Route<InviteResponse>(HttpMethod.Delete, "/invites/$InviteCode", InviteResponse.serializer())

    object CurrentUserGet
        : Route<DiscordUser>(HttpMethod.Get, "/users/@me", DiscordUser.serializer())

    object CurrentUserPatch
        : Route<DiscordUser>(HttpMethod.Patch, "/users/@me", DiscordUser.serializer())

    object UserGet
        : Route<DiscordUser>(HttpMethod.Get, "/users/$UserId", DiscordUser.serializer())

    object CurrentUsersGuildsGet
        : Route<List<DiscordPartialGuild>>(HttpMethod.Get, "/users/@me/guilds", ListSerializer(DiscordPartialGuild.serializer()))

    object GuildLeave
        : Route<Unit>(HttpMethod.Delete, "/users/@me/guilds/$GuildId", NoStrategy)

    object DMPost
        : Route<DiscordChannel>(HttpMethod.Post, "/users/@me/channels", DiscordChannel.serializer())

    object UserConnectionsGet
        : Route<List<Connection>>(HttpMethod.Get, "/users/@me/connections", ListSerializer(Connection.serializer()))

    object GuildPost
        : Route<DiscordGuild>(HttpMethod.Post, "/guilds", DiscordGuild.serializer())

    object GuildGet
        : Route<DiscordGuild>(HttpMethod.Get, "/guilds/$GuildId", DiscordGuild.serializer())

    object GuildPatch
        : Route<DiscordGuild>(HttpMethod.Patch, "/guilds/$GuildId", DiscordGuild.serializer())

    object GuildDelete
        : Route<Unit>(HttpMethod.Delete, "/guilds/$GuildId", NoStrategy)

    object GuildChannelsGet
        : Route<List<DiscordChannel>>(HttpMethod.Get, "/guilds/$GuildId/channels", ListSerializer(DiscordChannel.serializer()))

    object GuildChannelsPost
        : Route<DiscordChannel>(HttpMethod.Post, "/guilds/$GuildId/channels", DiscordChannel.serializer())

    object GuildChannelsPatch
        : Route<Unit>(HttpMethod.Patch, "/guilds/$GuildId/channels", NoStrategy)

    object GuildMemberGet
        : Route<DiscordGuildMember>(HttpMethod.Get, "/guilds/$GuildId/members/$UserId", DiscordGuildMember.serializer())

    object GuildMembersGet
        : Route<List<DiscordGuildMember>>(HttpMethod.Get, "/guilds/$GuildId/members", ListSerializer(DiscordGuildMember.serializer()))

    object GuildMemberPut
        : Route<DiscordGuildMember?>(HttpMethod.Put, "/guilds/$GuildId/members/$UserId", DiscordGuildMember.serializer().optional)

    object GuildMemberPatch
        : Route<Unit>(HttpMethod.Patch, "/guilds/$GuildId/members/$UserId", NoStrategy)

    object GuildCurrentUserNickPatch
        : Route<String>(HttpMethod.Patch, "/guilds/$GuildId/members/@me/nick", String.serializer())

    object GuildMemberRolePut
        : Route<Unit>(HttpMethod.Put, "/guilds/$GuildId/members/$UserId/roles/$RoleId", NoStrategy)

    object GuildMemberRoleDelete
        : Route<Unit>(HttpMethod.Delete, "/guilds/$GuildId/members/$UserId/roles/$RoleId", NoStrategy)

    object GuildMemberDelete
        : Route<Unit>(HttpMethod.Delete, "/guilds/$GuildId/members/$UserId", NoStrategy)

    object GuildBansGet
        : Route<List<BanResponse>>(HttpMethod.Get, "/guilds/$GuildId/bans", ListSerializer(BanResponse.serializer()))

    object GuildBanGet
        : Route<BanResponse>(HttpMethod.Get, "/guilds/$GuildId/bans/$UserId", BanResponse.serializer())

    object GuildBanPut
        : Route<Unit>(HttpMethod.Put, "/guilds/$GuildId/bans/$UserId", NoStrategy)

    object GuildBanDelete
        : Route<Unit>(HttpMethod.Delete, "/guilds/$GuildId/bans/$UserId", NoStrategy)

    object GuildRolesGet
        : Route<List<DiscordRole>>(HttpMethod.Get, "/guilds/$GuildId/roles", ListSerializer(DiscordRole.serializer()))

    object GuildRolePost
        : Route<DiscordRole>(HttpMethod.Post, "/guilds/$GuildId/roles", DiscordRole.serializer())

    object GuildRolesPatch
        : Route<List<DiscordRole>>(HttpMethod.Patch, "/guilds/$GuildId/roles", ListSerializer(DiscordRole.serializer()))

    object GuildRolePatch
        : Route<DiscordRole>(HttpMethod.Patch, "/guilds/$GuildId/roles/$RoleId", DiscordRole.serializer())

    object GuildRoleDelete
        : Route<Unit>(HttpMethod.Delete, "/guilds/$GuildId/roles/$RoleId", NoStrategy)

    object GuildPruneCountGet
        : Route<GetPruneResponse>(HttpMethod.Get, "/guilds/$GuildId/prune", GetPruneResponse.serializer())

    object GuildPrunePost
        : Route<PruneResponse>(HttpMethod.Post, "/guilds/$GuildId/prune", PruneResponse.serializer())

    object GuildVoiceRegionsGet
        : Route<List<VoiceRegion>>(HttpMethod.Get, "/guilds/$GuildId/regions", ListSerializer(VoiceRegion.serializer()))

    object GuildInvitesGet
        : Route<List<InviteResponse>>(HttpMethod.Get, "/guilds/$GuildId/invites", ListSerializer(InviteResponse.serializer()))

    object GuildIntegrationGet
        : Route<List<IntegrationResponse>>(HttpMethod.Get, "/guilds/$GuildId/integrations", ListSerializer(IntegrationResponse.serializer()))

    object GuildIntegrationPost
        : Route<Unit>(HttpMethod.Post, "/guilds/$GuildId/integrations", NoStrategy)

    object GuildIntegrationPatch
        : Route<Unit>(HttpMethod.Patch, "/guilds/$GuildId/integrations/$IntegrationId", NoStrategy)

    object GuildIntegrationDelete
        : Route<Unit>(HttpMethod.Delete, "/guilds/$GuildId/integrations/$IntegrationId", NoStrategy)

    object GuildIntegrationSyncPost
        : Route<Unit>(HttpMethod.Post, "/guilds/$GuildId/integrations/$IntegrationId/sync", NoStrategy)

    object GuildEmbedGet
        : Route<EmbedResponse>(HttpMethod.Get, "/guilds/$GuildId/embed", EmbedResponse.serializer())

    object GuildEmbedPatch
        : Route<EmbedResponse>(HttpMethod.Patch, "/guilds/$GuildId/embed", EmbedResponse.serializer())

    object GuildVanityInviteGet
        : Route<InviteResponse>(HttpMethod.Get, "/guilds/$GuildId/vanity-url", InviteResponse.serializer())

    @KordPreview
    object MessageCrosspost
        : Route<DiscordMessage>(HttpMethod.Post, "/channels/$ChannelId/messages/$MessageId/crosspost", DiscordMessage.serializer())

    @KordPreview
    object NewsChannelFollow
        : Route<FollowedChannelResponse>(HttpMethod.Post, "/channels/$ChannelId/followers", FollowedChannelResponse.serializer())

    //TODO must return an image
    object GuildWidgetGet
        : Route<Unit>(HttpMethod.Get, "/guilds/$GuildId/widget", NoStrategy)

    /**
     * Returns the guild preview object for the given id, even if the user is not in the guild.
     *
     * This endpoint is only for Public guilds.
     */
    object GuildPreviewGet
        : Route<DiscordGuildPreview>(HttpMethod.Get, "/guilds/${GuildId}/preview", DiscordGuildPreview.serializer())

    object ChannelWebhooksGet
        : Route<List<DiscordWebhook>>(HttpMethod.Get, "/channels/$ChannelId/webhooks", ListSerializer(DiscordWebhook.serializer()))

    object GuildWebhooksGet
        : Route<List<DiscordWebhook>>(HttpMethod.Get, "/guild/$GuildId/webhooks", ListSerializer(DiscordWebhook.serializer()))

    object WebhookGet
        : Route<DiscordWebhook>(HttpMethod.Get, "/webhooks/$WebhookId", DiscordWebhook.serializer())

    object WebhookPost
        : Route<DiscordWebhook>(HttpMethod.Post, "/channels/$ChannelId/webhooks", DiscordWebhook.serializer())

    object WebhookByTokenGet
        : Route<DiscordWebhook>(HttpMethod.Get, "/webhooks/$WebhookId/$WebhookToken", DiscordWebhook.serializer())

    object WebhookPatch
        : Route<DiscordWebhook>(HttpMethod.Patch, "/webhooks/$WebhookId", DiscordWebhook.serializer())

    object WebhookByTokenPatch
        : Route<DiscordWebhook>(HttpMethod.Patch, "/webhooks/$WebhookId/$WebhookToken", DiscordWebhook.serializer())

    object WebhookDelete
        : Route<Unit>(HttpMethod.Delete, "/webhooks/$WebhookId", NoStrategy)

    object WebhookByTokenDelete
        : Route<Unit>(HttpMethod.Delete, "/webhooks/$WebhookId/$WebhookToken", NoStrategy)

    //TODO Make sure of the return of these routes below

    object ExecuteWebhookPost
        : Route<DiscordMessage?>(HttpMethod.Post, "/webhooks/$WebhookId/$WebhookToken", DiscordMessage.serializer().optional)

    object ExecuteSlackWebhookPost
        : Route<Unit>(HttpMethod.Post, "/webhooks/$WebhookId/$WebhookToken/slack", NoStrategy)

    object ExecuteGithubWebhookPost
        : Route<Unit>(HttpMethod.Post, "/webhooks/$WebhookId/$WebhookToken", NoStrategy)

    object VoiceRegionsGet
        : Route<List<VoiceRegion>>(HttpMethod.Get, "/voice/regions", ListSerializer(VoiceRegion.serializer()))

    object CurrentApplicationInfo
        : Route<ApplicationInfoResponse>(HttpMethod.Get, "/oauth2/applications/@me", ApplicationInfoResponse.serializer())

    companion object {
        val baseUrl = "https://discord.com/api/$restVersion"
    }

    open class Key(val identifier: String, val isMajor: Boolean = false) {
        override fun toString(): String = identifier
    }

    object GuildId : Key("{guild.id}", true)
    object ChannelId : Key("{channel.id}", true)
    object MessageId : Key("{message.id}")
    object Emoji : Key("{emoji}")
    object UserId : Key("{user.id}")
    object OverwriteId : Key("{overwrite.id}")
    object EmojiId : Key("{emoji.id}")
    object InviteCode : Key("{invite.code}")
    object RoleId : Key("{role.id}")
    object IntegrationId : Key("{integration.id}")
    object WebhookId : Key("{webhook.id}", true)
    object WebhookToken : Key("{webhook.token}")

}

internal object NoStrategy : DeserializationStrategy<Unit> {
    @OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class)
    override val descriptor: SerialDescriptor
        get() = buildSerialDescriptor("NoStrategy", StructureKind.OBJECT) {}

    override fun deserialize(decoder: Decoder) {}

    override fun patch(decoder: Decoder, old: Unit) {}
}

package com.gitlab.kordlib.rest.service

import com.gitlab.kordlib.rest.json.response.ApplicationInfoResponse
import com.gitlab.kordlib.rest.request.RequestHandler
import com.gitlab.kordlib.rest.route.Route

class ApplicationService(handler: RequestHandler) : RestService(handler) {

    suspend fun getCurrentApplicationInfo() : ApplicationInfoResponse = call(Route.CurrentApplicationInfo)

}

package com.gitlab.kordlib.rest.service

import com.gitlab.kordlib.rest.json.response.AuditLogEventResponse
import com.gitlab.kordlib.rest.request.RequestHandler
import com.gitlab.kordlib.rest.route.Route

class AuditLogService(requestHandler: RequestHandler) : RestService(requestHandler) {

    suspend fun getAuditLogs(
            guildId: String,
            userId: String? = null,
            action: AuditLogEventResponse? = null,
            before: String? = null,
            limit: Int = 50
    ) = call(Route.AuditLogGet) {
        keys[Route.GuildId] = guildId
        userId?.let { parameter("user_id", it) }
        action?.let { parameter("action_type", "${it.code}") }
        before?.let { parameter("before", it) }
        parameter("limit", "$limit")
        }
    }

package com.gitlab.kordlib.rest.service

import com.gitlab.kordlib.common.annotation.KordPreview
import com.gitlab.kordlib.common.entity.DiscordChannel
import com.gitlab.kordlib.common.entity.DiscordMessage
import com.gitlab.kordlib.rest.builder.channel.*
import com.gitlab.kordlib.rest.builder.message.MessageCreateBuilder
import com.gitlab.kordlib.rest.builder.message.MessageModifyBuilder
import com.gitlab.kordlib.rest.json.request.*
import com.gitlab.kordlib.rest.json.response.FollowedChannelResponse
import com.gitlab.kordlib.rest.json.response.InviteResponse
import com.gitlab.kordlib.rest.request.RequestHandler
import com.gitlab.kordlib.rest.route.Position
import com.gitlab.kordlib.rest.route.Route
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

class ChannelService(requestHandler: RequestHandler) : RestService(requestHandler) {

    @OptIn(ExperimentalContracts::class)
    suspend inline fun createMessage(channelId: String, builder: MessageCreateBuilder.() -> Unit): DiscordMessage {
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }
        return call(Route.MessagePost) {
            keys[Route.ChannelId] = channelId
            val multipartRequest = MessageCreateBuilder().apply(builder).toRequest()
            body(MessageCreateRequest.serializer(), multipartRequest.request)
            multipartRequest.files.forEach { file(it) }
        }
    }

    suspend fun getMessages(channelId: String, position: Position? = null, limit: Int = 50) = call(Route.MessagesGet) {
        keys[Route.ChannelId] = channelId
        if (position != null) {
            parameter(position.key, position.value)
        }
        parameter("limit", "$limit")

    }

    suspend fun getMessage(channelId: String, messageId: String) = call(Route.MessageGet) {
        keys[Route.MessageId] = messageId
        keys[Route.ChannelId] = channelId
    }

    suspend fun getChannelInvites(channelId: String) = call(Route.InvitesGet) {
        keys[Route.ChannelId] = channelId
    }

    suspend fun getChannel(channelId: String) = call(Route.ChannelGet) {
        keys[Route.ChannelId] = channelId
    }

    suspend fun addPinnedMessage(channelId: String, messageId: String) = call(Route.PinPut) {
        keys[Route.MessageId] = messageId
        keys[Route.ChannelId] = channelId
    }

    suspend fun getChannelPins(channelId: String) = call(Route.PinsGet) {
        keys[Route.ChannelId] = channelId
    }

    suspend fun createReaction(channelId: String, messageId: String, emoji: String) = call(Route.ReactionPut) {
        keys[Route.ChannelId] = channelId
        keys[Route.MessageId] = messageId
        keys[Route.Emoji] = emoji
    }

    suspend fun deleteReaction(channelId: String, messageId: String, userId: String, emoji: String) = call(Route.ReactionDelete) {
        keys[Route.ChannelId] = channelId
        keys[Route.MessageId] = messageId
        keys[Route.Emoji] = emoji
        keys[Route.UserId] = userId
    }

    suspend fun deleteOwnReaction(channelId: String, messageId: String, emoji: String) = call(Route.OwnReactionDelete) {
        keys[Route.ChannelId] = channelId
        keys[Route.MessageId] = messageId
        keys[Route.Emoji] = emoji
    }

    suspend fun deleteAllReactionsForEmoji(channelId: String, messageId: String, emoji: String) = call(Route.DeleteAllReactionsForEmoji) {
        keys[Route.ChannelId] = channelId
        keys[Route.MessageId] = messageId
        keys[Route.Emoji] = emoji
    }

    suspend fun deletePinnedMessage(channelId: String, messageId: String) = call(Route.PinDelete) {
        keys[Route.ChannelId] = channelId
        keys[Route.MessageId] = messageId
    }

    suspend fun deleteAllReactions(channelId: String, messageId: String) = call(Route.AllReactionsDelete) {
        keys[Route.ChannelId] = channelId
        keys[Route.MessageId] = messageId
    }

    suspend fun deleteMessage(channelId: String, messageId: String, reason: String? = null) = call(Route.MessageDelete) {
        keys[Route.ChannelId] = channelId
        keys[Route.MessageId] = messageId
        reason?.let { header("X-Audit-Log-Reason", reason) }
    }

    suspend fun bulkDelete(channelId: String, messages: BulkDeleteRequest) = call(Route.BulkMessageDeletePost) {
        keys[Route.ChannelId] = channelId
        body(BulkDeleteRequest.serializer(), messages)
    }

    suspend fun deleteChannel(channelId: String, reason: String? = null) = call(Route.ChannelDelete) {
        keys[Route.ChannelId] = channelId
        reason?.let { header("X-Audit-Log-Reason", reason) }
    }

    suspend fun deleteChannelPermission(channelId: String, overwriteId: String, reason: String? = null) = call(Route.ChannelPermissionDelete) {
        keys[Route.ChannelId] = channelId
        keys[Route.OverwriteId] = overwriteId
        reason?.let { header("X-Audit-Log-Reason", reason) }
    }

    suspend fun editChannelPermissions(channelId: String, overwriteId: String, permissions: ChannelPermissionEditRequest, reason: String? = null) = call(Route.ChannelPermissionPut) {
        keys[Route.ChannelId] = channelId
        keys[Route.OverwriteId] = overwriteId
        body(ChannelPermissionEditRequest.serializer(), permissions)
        reason?.let { header("X-Audit-Log-Reason", reason) }
    }

    suspend fun getReactions(channelId: String, messageId: String, emoji: String, position: Position? = null, limit: Int = 25) = call(Route.ReactionsGet) {
        keys[Route.ChannelId] = channelId
        keys[Route.MessageId] = messageId
        keys[Route.Emoji] = emoji

        if (position != null) {
            parameter(position.key, position.value)
        }
        parameter("limit", "$limit")
    }

    suspend fun triggerTypingIndicator(channelId: String) = call(Route.TypingIndicatorPost) {
        keys[Route.ChannelId] = channelId
    }

    suspend fun removeFromGroup(channelId: String, userId: String) = call(Route.GroupDMUserDelete) {
        keys[Route.ChannelId] = channelId
        keys[Route.UserId] = userId
    }

    suspend fun addToGroup(channelId: String, userId: String, addUser: UserAddDMRequest) = call(Route.GroupDMUserPut) {
        keys[Route.ChannelId] = channelId
        keys[Route.UserId] = userId
        body(UserAddDMRequest.serializer(), addUser)
    }

    @OptIn(ExperimentalContracts::class)
    suspend inline fun createInvite(channelId: String, builder: InviteCreateBuilder.() -> Unit = {}): InviteResponse {
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }
        return call(Route.InvitePost) {
            keys[Route.ChannelId] = channelId
            val request = InviteCreateBuilder().apply(builder)
            body(InviteCreateRequest.serializer(), request.toRequest())
            request.reason?.let { header("X-Audit-Log-Reason", it) }
        }
    }

    @OptIn(ExperimentalContracts::class)
    suspend inline fun editMessage(channelId: String, messageId: String, builder: MessageModifyBuilder.() -> Unit): DiscordMessage {
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }
        return call(Route.EditMessagePatch) {
            keys[Route.ChannelId] = channelId
            keys[Route.MessageId] = messageId
            body(MessageEditPatchRequest.serializer(), MessageModifyBuilder().apply(builder).toRequest())
        }
    }

    suspend fun putChannel(channelId: String, channel: ChannelModifyPutRequest, reason: String? = null) = call(Route.ChannelPut) {
        keys[Route.ChannelId] = channelId
        body(ChannelModifyPutRequest.serializer(), channel)
        reason?.let { header("X-Audit-Log-Reason", reason) }
    }

    suspend fun patchChannel(channelId: String, channel: ChannelModifyPatchRequest, reason: String? = null) = call(Route.ChannelPatch) {
        keys[Route.ChannelId] = channelId
        body(ChannelModifyPatchRequest.serializer(), channel)
        reason?.let { header("X-Audit-Log-Reason", reason) }
    }

    @KordPreview
    suspend fun crossPost(channelId: String, messageId: String) : DiscordMessage = call(Route.MessageCrosspost) {
        keys[Route.ChannelId] = channelId
        keys[Route.MessageId] = messageId
    }

    @KordPreview
    suspend fun followNewsChannel(channelId: String, request: ChannelFollowRequest): FollowedChannelResponse = call(Route.NewsChannelFollow) {
        keys[Route.ChannelId] = channelId
        body(ChannelFollowRequest.serializer(), request)
    }

}

@OptIn(ExperimentalContracts::class)
suspend inline fun ChannelService.patchTextChannel(channelId: String, builder: TextChannelModifyBuilder.() -> Unit): DiscordChannel {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    return patchChannel(channelId, TextChannelModifyBuilder().apply(builder).toRequest())
}

@OptIn(ExperimentalContracts::class)
suspend inline fun ChannelService.patchVoiceChannel(channelId: String, builder: VoiceChannelModifyBuilder.() -> Unit) : DiscordChannel {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    return patchChannel(channelId, VoiceChannelModifyBuilder().apply(builder).toRequest())
}

@OptIn(ExperimentalContracts::class)
suspend inline fun ChannelService.patchStoreChannel(channelId: String, builder: StoreChannelModifyBuilder.() -> Unit): DiscordChannel {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    return patchChannel(channelId, StoreChannelModifyBuilder().apply(builder).toRequest())
}

@OptIn(ExperimentalContracts::class)
suspend inline fun ChannelService.patchNewsChannel(channelId: String, builder: NewsChannelModifyBuilder.() -> Unit): DiscordChannel {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    return patchChannel(channelId, NewsChannelModifyBuilder().apply(builder).toRequest())
}

@OptIn(ExperimentalContracts::class)
suspend inline fun ChannelService.patchCategory(channelId: String, builder: CategoryModifyBuilder.() -> Unit) : DiscordChannel {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    return patchChannel(channelId, CategoryModifyBuilder().apply(builder).toRequest())
}

@OptIn(ExperimentalContracts::class)
suspend inline fun ChannelService.editMemberPermissions(channelId: String, memberId: String, builder: ChannelPermissionModifyBuilder.() -> Unit) {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    val modifyBuilder = ChannelPermissionModifyBuilder("member").apply(builder)
    editChannelPermissions(channelId, memberId, modifyBuilder.toRequest(), modifyBuilder.reason)
}

@OptIn(ExperimentalContracts::class)
suspend inline fun ChannelService.editRolePermission(channelId: String, roleId: String, builder: ChannelPermissionModifyBuilder.() -> Unit) {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    val modifyBuilder = ChannelPermissionModifyBuilder("role").apply(builder)
    editChannelPermissions(channelId, roleId, modifyBuilder.toRequest(), modifyBuilder.reason)
}

package com.gitlab.kordlib.rest.service

import com.gitlab.kordlib.common.entity.DiscordEmoji
import com.gitlab.kordlib.rest.builder.guild.EmojiCreateBuilder
import com.gitlab.kordlib.rest.builder.guild.EmojiModifyBuilder
import com.gitlab.kordlib.rest.json.request.EmojiCreateRequest
import com.gitlab.kordlib.rest.json.request.EmojiModifyRequest
import com.gitlab.kordlib.rest.request.RequestHandler
import com.gitlab.kordlib.rest.route.Route
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

class EmojiService(requestHandler: RequestHandler) : RestService(requestHandler) {

    @OptIn(ExperimentalContracts::class)
    suspend inline fun createEmoji(guildId: String, builder: EmojiCreateBuilder.() -> Unit): DiscordEmoji {
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }
        return call(Route.GuildEmojiPost) {
            keys[Route.GuildId] = guildId
            val emoji = EmojiCreateBuilder().apply(builder)

            body(EmojiCreateRequest.serializer(), emoji.toRequest())
            emoji.reason?.let { header("X-Audit-Log-Reason", it) }
        }
    }

    @Deprecated("use the inline builder instead", ReplaceWith("createEmoji(guildId) {  }"), level = DeprecationLevel.WARNING)
    suspend fun createEmoji(guildId: String, emoji: EmojiCreateRequest, reason: String? = null) = call(Route.GuildEmojiPost) {
        keys[Route.GuildId] = guildId
        body(EmojiCreateRequest.serializer(), emoji)
        reason?.let { header("X-Audit-Log-Reason", reason) }
    }

    suspend fun deleteEmoji(guildId: String, emojiId: String, reason: String? = null) = call(Route.GuildEmojiDelete) {
        keys[Route.GuildId] = guildId
        keys[Route.EmojiId] = emojiId
        reason?.let { header("X-Audit-Log-Reason", reason) }
    }

    @OptIn(ExperimentalContracts::class)
    suspend inline fun modifyEmoji(guildId: String, emojiId: String, builder: EmojiModifyBuilder.() -> Unit): DiscordEmoji {
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }
        return call(Route.GuildEmojiPatch) {
            keys[Route.GuildId] = guildId
            keys[Route.EmojiId] = emojiId
            val modifyBuilder = EmojiModifyBuilder().apply(builder)
            body(EmojiModifyRequest.serializer(), modifyBuilder.toRequest())
            modifyBuilder.reason?.let { header("X-Audit-Log-Reason", it) }
        }
    }

    suspend fun getEmoji(guildId: String, emojiId: String) = call(Route.GuildEmojiGet) {
        keys[Route.GuildId] = guildId
        keys[Route.EmojiId] = emojiId
    }

    suspend fun getEmojis(guildId: String) = call(Route.GuildEmojisGet) {
        keys[Route.GuildId] = guildId
    }
}

package com.gitlab.kordlib.rest.service

import com.gitlab.kordlib.common.entity.DiscordChannel
import com.gitlab.kordlib.common.entity.DiscordGuild
import com.gitlab.kordlib.common.entity.DiscordRole
import com.gitlab.kordlib.rest.builder.ban.BanCreateBuilder
import com.gitlab.kordlib.rest.builder.channel.*
import com.gitlab.kordlib.rest.builder.guild.GuildCreateBuilder
import com.gitlab.kordlib.rest.builder.guild.GuildModifyBuilder
import com.gitlab.kordlib.rest.builder.integration.IntegrationModifyBuilder
import com.gitlab.kordlib.rest.builder.member.MemberAddBuilder
import com.gitlab.kordlib.rest.builder.member.MemberModifyBuilder
import com.gitlab.kordlib.rest.builder.role.RoleCreateBuilder
import com.gitlab.kordlib.rest.builder.role.RoleModifyBuilder
import com.gitlab.kordlib.rest.builder.role.RolePositionsModifyBuilder
import com.gitlab.kordlib.rest.json.request.*
import com.gitlab.kordlib.rest.request.RequestHandler
import com.gitlab.kordlib.rest.route.Position
import com.gitlab.kordlib.rest.route.Route
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

class GuildService(requestHandler: RequestHandler) : RestService(requestHandler) {

    @OptIn(ExperimentalContracts::class)
    suspend inline fun createGuild(builder: GuildCreateBuilder.() -> Unit): DiscordGuild {
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }
        return call(Route.GuildPost) {
            body(GuildCreateRequest.serializer(), GuildCreateBuilder().apply(builder).toRequest())
        }
    }

    /**
     * @param withCounts whether to include the [DiscordGuild.approximateMemberCount]
     * and [DiscordGuild.approximatePresenceCount] fields, `false` by default.
     */
    suspend fun getGuild(guildId: String, withCounts: Boolean = false) = call(Route.GuildGet) {
        keys[Route.GuildId] = guildId
        parameter("with_count", withCounts.toString())
    }

    /**
     * Returns the preview of this [guildId].
     */
    suspend fun getGuildPreview(guildId: String) = call(Route.GuildPreviewGet) {
        keys[Route.GuildId] = guildId
    }

    @OptIn(ExperimentalContracts::class)
    suspend inline fun modifyGuild(guildId: String, builder: GuildModifyBuilder.() -> Unit): DiscordGuild {
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }

        return call(Route.GuildPatch) {
            keys[Route.GuildId] = guildId
            val modifyBuilder = GuildModifyBuilder().apply(builder)
            body(GuildModifyRequest.serializer(), modifyBuilder.toRequest())
            modifyBuilder.reason?.let { header("X-Audit-Log-Reason", it) }
        }
    }

    suspend fun deleteGuild(guildId: String) = call(Route.GuildDelete) {
        keys[Route.GuildId] = guildId
    }

    suspend fun getGuildChannels(guildId: String) = call(Route.GuildChannelsGet) {
        keys[Route.GuildId] = guildId
    }

    suspend fun createGuildChannel(guildId: String, channel: GuildCreateChannelRequest, reason: String? = null) = call(Route.GuildChannelsPost) {
        keys[Route.GuildId] = guildId
        body(GuildCreateChannelRequest.serializer(), channel)
        reason?.let { header("X-Audit-Log-Reason", it) }
    }

    @OptIn(ExperimentalContracts::class)
    suspend inline fun modifyGuildChannelPosition(guildId: String, builder: GuildChannelPositionModifyBuilder.() -> Unit) {
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }

        call(Route.GuildChannelsPatch) {
            keys[Route.GuildId] = guildId
            val modifyBuilder = GuildChannelPositionModifyBuilder().apply(builder)
            body(GuildChannelPositionModifyRequest.Serializer, modifyBuilder.toRequest())
            modifyBuilder.reason?.let { header("X-Audit-Log-Reason", it) }
        }
    }

    suspend fun getGuildMember(guildId: String, userId: String) = call(Route.GuildMemberGet) {
        keys[Route.GuildId] = guildId
        keys[Route.UserId] = userId
    }

    suspend fun getGuildMembers(guildId: String, position: Position? = null, limit: Int = 1) = call(Route.GuildMembersGet) {
        keys[Route.GuildId] = guildId
        if (position != null) {
            parameter(position.key, position.value)
        }
        parameter("limit", "$limit")
    }

    suspend fun addGuildMember(guildId: String, userId: String, builder: MemberAddBuilder.() -> Unit) = call(Route.GuildMemberPut) {
        keys[Route.GuildId] = guildId
        keys[Route.UserId] = userId
        body(GuildMemberAddRequest.serializer(), MemberAddBuilder().also(builder).toRequest())
    }

    @OptIn(ExperimentalContracts::class)
    suspend inline fun modifyGuildMember(guildId: String, userId: String, builder: MemberModifyBuilder.() -> Unit) {
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }

        call(Route.GuildMemberPatch) {
            keys[Route.GuildId] = guildId
            keys[Route.UserId] = userId
            val modifyBuilder = MemberModifyBuilder().apply(builder)
            body(GuildMemberModifyRequest.serializer(), modifyBuilder.toRequest())
            modifyBuilder.reason?.let { header("X-Audit-Log-Reason", it) }
        }
    }

    suspend fun addRoleToGuildMember(guildId: String, userId: String, roleId: String, reason: String? = null) = call(Route.GuildMemberRolePut) {
        keys[Route.GuildId] = guildId
        keys[Route.UserId] = userId
        keys[Route.RoleId] = roleId
        reason?.let { header("X-Audit-Log-Reason", it) }
    }

    suspend fun deleteRoleFromGuildMember(guildId: String, userId: String, roleId: String, reason: String? = null) = call(Route.GuildMemberRoleDelete) {
        keys[Route.GuildId] = guildId
        keys[Route.UserId] = userId
        keys[Route.RoleId] = roleId
        reason?.let { header("X-Audit-Log-Reason", it) }
    }

    suspend fun deleteGuildMember(guildId: String, userId: String, reason: String? = null) = call(Route.GuildMemberDelete) {
        keys[Route.GuildId] = guildId
        keys[Route.UserId] = userId
        reason?.let { header("X-Audit-Log-Reason", it) }
    }

    suspend fun getGuildBans(guildId: String) = call(Route.GuildBansGet) {
        keys[Route.GuildId] = guildId
    }

    suspend fun getGuildBan(guildId: String, userId: String) = call(Route.GuildBanGet) {
        keys[Route.GuildId] = guildId
        keys[Route.UserId] = userId
    }

    @OptIn(ExperimentalContracts::class)
    suspend inline fun addGuildBan(guildId: String, userId: String, builder: BanCreateBuilder.() -> Unit) {
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }

        call(Route.GuildBanPut) {
            keys[Route.GuildId] = guildId
            keys[Route.UserId] = userId
            val createBuilder = BanCreateBuilder().apply(builder)
            body(GuildBanAddRequest.serializer(), createBuilder.toRequest())
            createBuilder.reason?.let { header("X-Audit-Log-Reason", it) }
        }
    }

    suspend fun deleteGuildBan(guildId: String, userId: String, reason: String? = null) = call(Route.GuildBanDelete) {
        keys[Route.GuildId] = guildId
        keys[Route.UserId] = userId
        reason?.let { header("X-Audit-Log-Reason", it) }
    }

    suspend fun getGuildRoles(guildId: String) = call(Route.GuildRolesGet) {
        keys[Route.GuildId] = guildId
    }

    @OptIn(ExperimentalContracts::class)
    suspend inline fun createGuildRole(guildId: String, builder: RoleCreateBuilder.() -> Unit = {}): DiscordRole {
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }

        return call(Route.GuildRolePost) {
            keys[Route.GuildId] = guildId
            val createBuilder = RoleCreateBuilder().apply(builder)
            body(GuildRoleCreateRequest.serializer(), createBuilder.toRequest())
            createBuilder.reason?.let { header("X-Audit-Log-Reason", it) }
        }
    }

    suspend inline fun modifyGuildRolePosition(guildId: String, builder: RolePositionsModifyBuilder.() -> Unit) = call(Route.GuildRolesPatch) {
        keys[Route.GuildId] = guildId
        val modifyBuilder = RolePositionsModifyBuilder().apply(builder)
        body(GuildRolePositionModifyRequest.Serializer, modifyBuilder.toRequest())
        modifyBuilder.reason?.let { header("X-Audit-Log-Reason", it) }
    }

    suspend inline fun modifyGuildRole(guildId: String, roleId: String, builder: RoleModifyBuilder.() -> Unit) = call(Route.GuildRolePatch) {
        keys[Route.GuildId] = guildId
        keys[Route.RoleId] = roleId
        val modifyBuilder = RoleModifyBuilder().apply(builder)
        body(GuildRoleModifyRequest.serializer(), modifyBuilder.toRequest())
        modifyBuilder.reason?.let { header("X-Audit-Log-Reason", it) }
    }

    suspend fun deleteGuildRole(guildId: String, roleId: String, reason: String? = null) = call(Route.GuildRoleDelete) {
        keys[Route.GuildId] = guildId
        keys[Route.RoleId] = roleId
        reason?.let { header("X-Audit-Log-Reason", it) }
    }

    suspend fun getGuildPruneCount(guildId: String, days: Int = 7) = call(Route.GuildPruneCountGet) {
        keys[Route.GuildId] = guildId
        parameter("days", days)
    }

    suspend fun beginGuildPrune(guildId: String, days: Int = 7, computePruneCount: Boolean = true, reason: String? = null) = call(Route.GuildPrunePost) {
        keys[Route.GuildId] = guildId
        parameter("days", days)
        parameter("compute_prune_count", computePruneCount)
        reason?.let { header("X-Audit-Log-Reason", it) }
    }

    suspend fun getGuildVoiceRegions(guildId: String) = call(Route.GuildVoiceRegionsGet) {
        keys[Route.GuildId] = guildId
    }

    suspend fun getGuildInvites(guildId: String) = call(Route.GuildInvitesGet) {
        keys[Route.GuildId] = guildId
    }

    suspend fun getGuildIntegrations(guildId: String) = call(Route.GuildIntegrationGet) {
        keys[Route.GuildId] = guildId
    }

    suspend fun createGuildIntegration(guildId: String, integration: GuildIntegrationCreateRequest) = call(Route.GuildIntegrationPost) {
        keys[Route.GuildId] = guildId
        body(GuildIntegrationCreateRequest.serializer(), integration)
    }

    suspend inline fun modifyGuildIntegration(guildId: String, integrationId: String, builder: IntegrationModifyBuilder.() -> Unit) = call(Route.GuildIntegrationPatch) {
        keys[Route.GuildId] = guildId
        keys[Route.IntegrationId] = integrationId
        body(GuildIntegrationModifyRequest.serializer(), IntegrationModifyBuilder().apply(builder).toRequest())
    }

    suspend fun deleteGuildIntegration(guildId: String, integrationId: String) = call(Route.GuildIntegrationDelete) {
        keys[Route.GuildId] = guildId
        keys[Route.IntegrationId] = integrationId
    }

    suspend fun syncGuildIntegration(guildId: String, integrationId: String) = call(Route.GuildIntegrationSyncPost) {
        keys[Route.GuildId] = guildId
        keys[Route.IntegrationId] = integrationId
    }

    suspend fun getGuildEmbed(guildId: String) = call(Route.GuildEmbedGet) {
        keys[Route.GuildId] = guildId
    }

    suspend fun modifyGuildEmbed(guildId: String, embed: GuildEmbedModifyRequest) = call(Route.GuildEmbedPatch) {
        keys[Route.GuildId] = guildId
        body(GuildEmbedModifyRequest.serializer(), embed)
    }

    suspend fun getVanityInvite(guildId: String) = call(Route.GuildVanityInviteGet) {
        keys[Route.GuildId] = guildId
    }

    suspend fun modifyCurrentUserNickname(guildId: String, nick: CurrentUserNicknameModifyRequest) = call(Route.GuildCurrentUserNickPatch) {
        keys[Route.GuildId] = guildId
        body(CurrentUserNicknameModifyRequest.serializer(), nick)
    }

}

suspend inline fun GuildService.createTextChannel(guildId: String, builder: TextChannelCreateBuilder.() -> Unit): DiscordChannel {
    val createBuilder = TextChannelCreateBuilder().apply(builder)
    return createGuildChannel(guildId, createBuilder.toRequest(), createBuilder.reason)
}

suspend inline fun GuildService.createNewsChannel(guildId: String, builder: NewsChannelCreateBuilder.() -> Unit): DiscordChannel {
    val createBuilder = NewsChannelCreateBuilder().apply(builder)
    return createGuildChannel(guildId, createBuilder.toRequest(), createBuilder.reason)
}

suspend inline fun GuildService.createVoiceChannel(guildId: String, builder: VoiceChannelCreateBuilder.() -> Unit): DiscordChannel {
    val createBuilder = VoiceChannelCreateBuilder().apply(builder)
    return createGuildChannel(guildId, createBuilder.toRequest(), createBuilder.reason)
}

suspend inline fun GuildService.createCategory(guildId: String, builder: CategoryCreateBuilder.() -> Unit): DiscordChannel {
    val createBuilder = CategoryCreateBuilder().apply(builder)
    return createGuildChannel(guildId, createBuilder.toRequest(), createBuilder.reason)
}

package com.gitlab.kordlib.rest.service

import com.gitlab.kordlib.rest.request.RequestHandler
import com.gitlab.kordlib.rest.route.Route

class InviteService(requestHandler: RequestHandler) : RestService(requestHandler) {
    suspend fun getInvite(code: String, withCounts: Boolean) = call(Route.InviteGet) {
        keys[Route.InviteCode] = code
        parameter("with_counts", "$withCounts")
    }

    suspend fun deleteInvite(code: String, reason: String? = null) = call(Route.InviteDelete) {
        keys[Route.InviteCode] = code
        reason?.let { header("X-Audit-Log-Reason", it) }
    }
}

package com.gitlab.kordlib.rest.service

import com.gitlab.kordlib.common.annotation.KordExperimental
import com.gitlab.kordlib.common.annotation.KordUnsafe
import com.gitlab.kordlib.rest.request.KtorRequestHandler
import com.gitlab.kordlib.rest.request.RequestHandler
import com.gitlab.kordlib.rest.request.RequestBuilder
import com.gitlab.kordlib.rest.route.Route
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

class RestClient(requestHandler: RequestHandler) : RestService(requestHandler) {
    val auditLog: AuditLogService = AuditLogService(requestHandler)
    val channel: ChannelService = ChannelService(requestHandler)
    val emoji: EmojiService = EmojiService(requestHandler)
    val guild: GuildService = GuildService(requestHandler)
    val invite: InviteService = InviteService(requestHandler)
    val user: UserService = UserService(requestHandler)
    val voice: VoiceService = VoiceService(requestHandler)
    val webhook: WebhookService = WebhookService(requestHandler)
    val application: ApplicationService = ApplicationService(requestHandler)

    /**
     * Sends a request to the given [route]. This function exposes a direct call to the Discord api and allows
     * the user to send a custom [RequestBuilder.body].
     *
     * Unless such functionality is specifically needed, users are advised to use the safer [RestService] calls.
     *
     * @param route The route to which to send a request.
     * @param block The configuration for this request.
     */
    @OptIn(ExperimentalContracts::class)
    @KordUnsafe
    @KordExperimental
    suspend inline fun <T> unsafe(route: Route<T>, block: RequestBuilder<T>.() -> Unit): T {
        contract {
            callsInPlace(block, InvocationKind.EXACTLY_ONCE)
        }
        return call(route) {
            block()
        }
    }

    companion object {
        operator fun invoke(token: String) : RestClient {
            val requestHandler = KtorRequestHandler(token)
            return RestClient(requestHandler)
        }
    }
}

package com.gitlab.kordlib.rest.service

import com.gitlab.kordlib.rest.request.RequestHandler
import com.gitlab.kordlib.rest.request.RequestBuilder
import com.gitlab.kordlib.rest.route.Route
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

abstract class RestService(@PublishedApi internal val requestHandler: RequestHandler) {

    @OptIn(ExperimentalContracts::class)
    @PublishedApi
    internal suspend inline fun <T> call(route: Route<T>, builder: RequestBuilder<T>.() -> Unit = {}): T {
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }
        val request = RequestBuilder(route).apply(builder).build()
        return requestHandler.handle(request)
    }

}

package com.gitlab.kordlib.rest.service

import com.gitlab.kordlib.common.entity.DiscordChannel
import com.gitlab.kordlib.common.entity.DiscordUser
import com.gitlab.kordlib.rest.builder.user.CurrentUserModifyBuilder
import com.gitlab.kordlib.rest.builder.user.GroupDMCreateBuilder
import com.gitlab.kordlib.rest.json.request.CurrentUserModifyRequest
import com.gitlab.kordlib.rest.json.request.DMCreateRequest
import com.gitlab.kordlib.rest.json.request.GroupDMCreateRequest
import com.gitlab.kordlib.rest.request.RequestHandler
import com.gitlab.kordlib.rest.route.Position
import com.gitlab.kordlib.rest.route.Route
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

class UserService(requestHandler: RequestHandler) : RestService(requestHandler) {

    suspend fun getCurrentUser() = call(Route.CurrentUserGet)

    suspend fun getUser(userId: String) = call(Route.UserGet) {
        keys[Route.UserId] = userId
    }

    suspend fun getCurrentUserGuilds(position: Position? = null, limit: Int = 100) = call(Route.CurrentUsersGuildsGet) {
        if (position != null) {
            parameter(position.key, position.value)
        }

        parameter("limit", "$limit")
    }

    suspend fun leaveGuild(guildId: String) = call(Route.GuildLeave) {
        keys[Route.GuildId] = guildId
    }

    suspend fun getUserConnections() = call(Route.UserConnectionsGet)

    suspend fun createDM(dm: DMCreateRequest) = call(Route.DMPost) {
        body(DMCreateRequest.serializer(), dm)
    }

    @OptIn(ExperimentalContracts::class)
    suspend inline fun createGroupDM(builder: GroupDMCreateBuilder.() -> Unit): DiscordChannel {
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }

        return call(Route.DMPost) {
            body(GroupDMCreateRequest.serializer(), GroupDMCreateBuilder().apply(builder).toRequest())
        }
    }

    @OptIn(ExperimentalContracts::class)
    suspend inline fun modifyCurrentUser(builder: CurrentUserModifyBuilder.() -> Unit): DiscordUser {
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }

        return call(Route.CurrentUserPatch) {
            body(CurrentUserModifyRequest.serializer(), CurrentUserModifyBuilder().apply(builder).toRequest())
        }
    }

}

package com.gitlab.kordlib.rest.service

import com.gitlab.kordlib.rest.request.RequestHandler
import com.gitlab.kordlib.rest.route.Route

class VoiceService(requestHandler: RequestHandler) : RestService(requestHandler) {

    suspend fun getVoiceRegions() = call(Route.VoiceRegionsGet)

}

package com.gitlab.kordlib.rest.service

import com.gitlab.kordlib.common.annotation.KordExperimental
import com.gitlab.kordlib.common.entity.DiscordMessage
import com.gitlab.kordlib.common.entity.DiscordWebhook
import com.gitlab.kordlib.rest.builder.webhook.ExecuteWebhookBuilder
import com.gitlab.kordlib.rest.builder.webhook.WebhookCreateBuilder
import com.gitlab.kordlib.rest.builder.webhook.WebhookModifyBuilder
import com.gitlab.kordlib.rest.json.request.WebhookCreateRequest
import com.gitlab.kordlib.rest.json.request.WebhookExecuteRequest
import com.gitlab.kordlib.rest.json.request.WebhookModifyRequest
import com.gitlab.kordlib.rest.request.RequestHandler
import com.gitlab.kordlib.rest.route.Route
import kotlinx.serialization.json.JsonObject
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

class WebhookService(requestHandler: RequestHandler) : RestService(requestHandler) {

    @OptIn(ExperimentalContracts::class)
    suspend inline fun createWebhook(channelId: String, builder: WebhookCreateBuilder.() -> Unit): DiscordWebhook {
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }

        return call(Route.WebhookPost) {
            keys[Route.ChannelId] = channelId
            val createBuilder = WebhookCreateBuilder().apply(builder)
            body(WebhookCreateRequest.serializer(), createBuilder.toRequest())
            createBuilder.reason?.let { header("X-Audit-Log-Reason", it) }
        }
    }

    suspend fun getChannelWebhooks(channelId: String) = call(Route.ChannelWebhooksGet) {
        keys[Route.ChannelId] = channelId
    }

    suspend fun getGuildWebhooks(guildId: String) = call(Route.GuildWebhooksGet) {
        keys[Route.GuildId] = guildId
    }

    suspend fun getWebhook(webhookId: String) = call(Route.WebhookGet) {
        keys[Route.WebhookId] = webhookId
    }

    suspend fun getWebhookWithToken(webhookId: String, token: String) = call(Route.WebhookByTokenGet) {
        keys[Route.WebhookId] = webhookId
        keys[Route.WebhookToken] = token
    }

    @OptIn(ExperimentalContracts::class)
    suspend inline fun modifyWebhook(webhookId: String, builder: WebhookModifyBuilder.() -> Unit): DiscordWebhook {
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }

        return call(Route.WebhookPatch) {
            keys[Route.WebhookId] = webhookId
            val modifyBuilder = WebhookModifyBuilder().apply(builder)
            body(WebhookModifyRequest.serializer(), modifyBuilder.toRequest())
            modifyBuilder.reason?.let { header("X-Audit-Log-Reason", it) }
        }
    }

    @OptIn(ExperimentalContracts::class)
    suspend inline fun modifyWebhookWithToken(webhookId: String, token: String, builder: WebhookModifyBuilder.() -> Unit): DiscordWebhook {
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }

        return call(Route.WebhookByTokenPatch) {
            keys[Route.WebhookId] = webhookId
            keys[Route.WebhookToken] = token
            val modifyBuilder = WebhookModifyBuilder().apply(builder)
            body(WebhookModifyRequest.serializer(), modifyBuilder.toRequest())
            modifyBuilder.reason?.let { header("X-Audit-Log-Reason", it) }
        }
    }

    suspend fun deleteWebhook(webhookId: String, reason: String? = null) = call(Route.WebhookDelete) {
        keys[Route.WebhookId] = webhookId
        reason?.let { header("X-Audit-Log-Reason", reason) }
    }

    suspend fun deleteWebhookWithToken(webhookId: String, token: String, reason: String? = null) = call(Route.WebhookByTokenDelete) {
        keys[Route.WebhookId] = webhookId
        keys[Route.WebhookToken] = token
        reason?.let { header("X-Audit-Log-Reason", reason) }
    }

    @OptIn(ExperimentalContracts::class)
    suspend inline fun executeWebhook(webhookId: String, token: String, wait: Boolean, builder: ExecuteWebhookBuilder.() -> Unit): DiscordMessage? {
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }

        return call(Route.ExecuteWebhookPost) {
            keys[Route.WebhookId] = webhookId
            keys[Route.WebhookToken] = token
            parameter("wait", "$wait")
            val request = ExecuteWebhookBuilder().apply(builder).toRequest()
            body(WebhookExecuteRequest.serializer(), request.request)
            request.file?.let { file(it) }
        }
    }

    @KordExperimental
    suspend fun executeSlackWebhook(webhookId: String, token: String, body: JsonObject, wait: Boolean = false) =
            call(Route.ExecuteSlackWebhookPost) {
                keys[Route.WebhookId] = webhookId
                keys[Route.WebhookToken] = token
                parameter("wait", "$wait")
                body(JsonObject.serializer(), body)
            }

    @KordExperimental
    suspend fun executeGithubWebhook(webhookId: String, token: String, body: JsonObject, wait: Boolean = false) =
            call(Route.ExecuteGithubWebhookPost) {
                keys[Route.WebhookId] = webhookId
                keys[Route.WebhookToken] = token
                parameter("wait", "$wait")
                body(JsonObject.serializer(), body)
            }
}

package com.gitlab.kordlib.core.behavior.channel

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.cache.data.ChannelData
import com.gitlab.kordlib.core.entity.channel.CategorizableChannel
import com.gitlab.kordlib.core.entity.channel.Category
import com.gitlab.kordlib.core.entity.channel.Channel
import com.gitlab.kordlib.core.exception.EntityNotFoundException
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.core.supplier.getChannelOf
import com.gitlab.kordlib.rest.builder.channel.CategoryModifyBuilder
import com.gitlab.kordlib.rest.request.RestRequestException
import com.gitlab.kordlib.rest.service.patchCategory
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import java.util.*

/**
 * The behavior of a Discord category associated to a [guild].
 */
interface CategoryBehavior : GuildChannelBehavior {

    /**
     * Requests to get this behavior as a [Category].
     *
     * @throws [RequestException] if something went wrong during the request.
     * @throws [EntityNotFoundException] if the channel wasn't present.
     * @throws [ClassCastException] if the channel wasn't a category.
     */
    override suspend fun asChannel(): Category = supplier.getChannelOf(id)

    /**
     * Requests to get this behavior as a [Category],
     * returns null if the channel isn't present or is not a category.
     *
     * @throws [RequestException] if something went wrong during the request.
     * @throws [EntityNotFoundException] if the channel wasn't present.
     * @throws [ClassCastException] if the channel wasn't a category.
     */
    override suspend fun asChannelOrNull(): Category? = supplier.getChannelOf(id)

    /**
     * Requests to get the channels that belong to this [Category].
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    val channels: Flow<CategorizableChannel>
        get() = supplier.getGuildChannels(guildId)
                .filterIsInstance<CategorizableChannel>()
                .filter { it.categoryId == id }

    /**
     * Returns a new [CategoryBehavior] with the given [strategy].
     */
    override fun withStrategy(
            strategy: EntitySupplyStrategy<*>
    ): CategoryBehavior = CategoryBehavior(guildId, id, kord, strategy)

    companion object {
        internal operator fun invoke(
                guildId: Snowflake,
                id: Snowflake,
                kord: Kord,
                strategy: EntitySupplyStrategy<*> = kord.resources.defaultStrategy
        ): CategoryBehavior = object : CategoryBehavior {
            override val guildId: Snowflake = guildId
            override val id: Snowflake = id
            override val kord: Kord = kord
            override val supplier: EntitySupplier = strategy.supply(kord)

            override fun hashCode(): Int = Objects.hash(id, guildId)

            override fun equals(other: Any?): Boolean = when(other) {
                is GuildChannelBehavior -> other.id == id && other.guildId == guildId
                is ChannelBehavior -> other.id == id
                else -> false
            }

        }
    }
}

/**
 * Requests to edit this category.
 *
 * @return The edited [Category].
 * @throws [RestRequestException] if something went wrong during the request.
 */
suspend fun CategoryBehavior.edit(builder: CategoryModifyBuilder.() -> Unit): Category {
    val response = kord.rest.channel.patchCategory(id.value, builder)
    val data = ChannelData.from(response)

    return Channel.from(data, kord) as Category
}

package com.gitlab.kordlib.core.behavior.channel

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.entity.Entity
import com.gitlab.kordlib.core.entity.Strategizable
import com.gitlab.kordlib.core.entity.channel.Channel
import com.gitlab.kordlib.core.exception.EntityNotFoundException
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.rest.request.RestRequestException
import java.util.*

/**
 * The behavior of a [Discord Channel](https://discord.com/developers/docs/resources/channel)
 */
interface ChannelBehavior : Entity, Strategizable {

    /**
     * This channel [formatted as a mention](https://discord.com/developers/docs/reference#message-formatting)
     * as used by the Discord API.
     */
    val mention get() = "<#${id.value}>"

    /**
     * Requests to get this behavior as a [Channel] .
     *
     * @throws [RequestException] if something went wrong during the request.
     * @throws [EntityNotFoundException] if the channel wasn't present.
     */
    suspend fun asChannel(): Channel = supplier.getChannel(id)

    /**
     * Requests to get this behavior as a [Channel],
     * returns null if the channel isn't present.
     *
     * @throws [RequestException] if something went wrong during the request.
     */
    suspend fun asChannelOrNull(): Channel? = supplier.getChannelOrNull(id)

    /**
     * Requests to delete a channel (or close it if this is a dm channel).
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun delete() {
        kord.rest.channel.deleteChannel(id.value)
    }

    /**
     * Returns a new [ChannelBehavior] with the given [strategy].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>) : ChannelBehavior = ChannelBehavior(id, kord, strategy)

    companion object {
        internal operator fun invoke(id: Snowflake, kord: Kord, strategy: EntitySupplyStrategy<*> = kord.resources.defaultStrategy) = object : ChannelBehavior {
            override val id: Snowflake = id
            override val kord: Kord = kord
            override val supplier: EntitySupplier = strategy.supply(kord)

            override fun hashCode(): Int = Objects.hash(id)

            override fun equals(other: Any?): Boolean = when(other) {
                is ChannelBehavior -> other.id == id
                else -> false
            }
        }
    }
}

package com.gitlab.kordlib.core.behavior.channel

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.GuildBehavior
import com.gitlab.kordlib.core.cache.data.InviteData
import com.gitlab.kordlib.core.entity.*
import com.gitlab.kordlib.core.entity.channel.GuildChannel
import com.gitlab.kordlib.core.exception.EntityNotFoundException
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.rest.builder.channel.ChannelPermissionModifyBuilder
import com.gitlab.kordlib.rest.request.RestRequestException
import com.gitlab.kordlib.rest.service.RestClient
import com.gitlab.kordlib.rest.service.editMemberPermissions
import com.gitlab.kordlib.rest.service.editRolePermission
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.withIndex
import java.util.*
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

/**
 * The behavior of a Discord channel associated to a [guild].
 */
interface GuildChannelBehavior : ChannelBehavior, Strategizable {

    /**
     * The id of the guild this channel is associated to.
     */
    val guildId: Snowflake

    /**
     * The guild behavior this channel is associated to.
     */
    val guild: GuildBehavior get() = GuildBehavior(guildId, kord)

    /**
     * Requests to get the invites of this channel.
     *
     * This property is not resolvable through cache and will always use the [RestClient] instead.
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    val invites: Flow<Invite>
        get() = flow {
            val responses = kord.rest.channel.getChannelInvites(id.value)

            for (response in responses) {
                val data = InviteData.from(response)

                emit(Invite(data, kord))
            }
        }

    /**
     * Requests to get this behavior as a [GuildChannel].
     *
     * @throws [RequestException] if something went wrong during the request.
     * @throws [EntityNotFoundException] if the channel wasn't present.
     * @throws [ClassCastException] if the channel isn't a guild channel.
     */
    override suspend fun asChannel(): GuildChannel = super.asChannel() as GuildChannel

    /**
     * Requests to get this behavior as a [GuildChannel],
     * returns null if the channel isn't present or if the channel isn't a guild channel.
     *
     * @throws [RequestException] if something went wrong during the request.
     */
    override suspend fun asChannelOrNull(): GuildChannel? = super.asChannelOrNull() as? GuildChannel

    /**
     * Requests to get this channel's [Guild].
     *
     * @throws [RequestException] if something went wrong during the request.
     * @throws [EntityNotFoundException] if the guild wasn't present.
     */
    suspend fun getGuild(): Guild = supplier.getGuild(guildId)

    /**
     * Requests to get this channel's [Guild],
     * returns null if the guild isn't present.
     *
     * @throws [RequestException] if something went wrong during the request.
     */
    suspend fun getGuildOrNull(): Guild? = supplier.getGuildOrNull(guildId)

    /**
     * Requests to add or replace a [PermissionOverwrite] to this entity.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun addOverwrite(overwrite: PermissionOverwrite) {
        kord.rest.channel.editChannelPermissions(channelId = id.value, overwriteId = overwrite.target.value, permissions = overwrite.asRequest())
    }

    /**
     * Requests to get the position of this channel in the [guild], as displayed in Discord,
     *.
     *
     * @throws [RequestException] if something went wrong during the request.
     */
    suspend fun getPosition(): Int = supplier.getGuildChannels(guildId).withIndex().first { it.value.id == id }.index

    override fun compareTo(other: Entity): Int {
        if (other !is GuildChannelBehavior) return super.compareTo(other)
        val discordOrder = compareBy<GuildChannelBehavior> { it.guildId }
                .thenBy { (it as? GuildChannel)?.guildId }
                .thenBy { it.id }

        return discordOrder.compare(this, other)
    }

    /**
     * Returns a new [GuildChannelBehavior] with the given [strategy].
     */
    override fun withStrategy(
            strategy: EntitySupplyStrategy<*>
    ): GuildChannelBehavior = GuildChannelBehavior(guildId, id, kord, strategy)

    companion object {
        internal operator fun invoke(
                guildId: Snowflake,
                id: Snowflake,
                kord: Kord,
                strategy: EntitySupplyStrategy<*> = kord.resources.defaultStrategy
        ): GuildChannelBehavior = object : GuildChannelBehavior {
            override val guildId: Snowflake = guildId
            override val id: Snowflake = id
            override val kord: Kord = kord
            override val supplier: EntitySupplier = strategy.supply(kord)

            override fun hashCode(): Int = Objects.hash(id, guildId)

            override fun equals(other: Any?): Boolean = when(other) {
                is GuildChannelBehavior -> other.id == id && other.guildId == guildId
                is ChannelBehavior -> other.id == id
                else -> false
            }
        }
    }

}

/**
 * Requests to add or replace a [PermissionOverwrite] for the [roleId].
 *
 *  @throws [RestRequestException] if something went wrong during the request.
 */
@OptIn(ExperimentalContracts::class)
suspend inline fun GuildChannelBehavior.editRolePermission(roleId: Snowflake, builder: ChannelPermissionModifyBuilder.() -> Unit) {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    kord.rest.channel.editRolePermission(channelId = id.value, roleId = roleId.value, builder = builder)
}

/**
 * Requests to add or replace a [PermissionOverwrite] for the [memberId].
 *
 * @throws [RestRequestException] if something went wrong during the request.
 */
@OptIn(ExperimentalContracts::class)
suspend inline fun GuildChannelBehavior.editMemberPermission(memberId: Snowflake, builder: ChannelPermissionModifyBuilder.() -> Unit) {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    kord.rest.channel.editMemberPermissions(channelId = id.value, memberId = memberId.value, builder = builder)
}

package com.gitlab.kordlib.core.behavior.channel

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.cache.data.WebhookData
import com.gitlab.kordlib.core.entity.Webhook
import com.gitlab.kordlib.core.entity.channel.GuildMessageChannel
import com.gitlab.kordlib.core.exception.EntityNotFoundException
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.rest.builder.webhook.WebhookCreateBuilder
import com.gitlab.kordlib.rest.json.request.BulkDeleteRequest
import com.gitlab.kordlib.rest.request.RestRequestException
import com.gitlab.kordlib.rest.service.RestClient
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import java.time.Duration
import java.time.Instant
import java.util.*
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.time.days
import kotlin.time.toJavaDuration

/**
 * The behavior of a Discord message channel associated to a [guild].
 */
interface GuildMessageChannelBehavior : GuildChannelBehavior, MessageChannelBehavior {

    /**
     * Requests to get all webhooks for this channel.
     *
     * This property is not resolvable through cache and will always use the [RestClient] instead.
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    val webhooks: Flow<Webhook>
        get() = flow {
            for (response in kord.rest.webhook.getChannelWebhooks(id.value)) {
                val data = WebhookData.from(response)
                emit(Webhook(data, kord))
            }
        }

    /**
     * Requests to get the this behavior as a [GuildMessageChannel].
     *
     * @throws [RequestException] if something went wrong during the request.
     * @throws [EntityNotFoundException] if the channel wasn't present.
     * @throws [ClassCastException] if the channel isn't a guild message channel.
     */
    override suspend fun asChannel(): GuildMessageChannel = super<GuildChannelBehavior>.asChannel() as GuildMessageChannel

    /**
     * Requests to get this behavior as a [GuildMessageChannel],
     * returns null if the channel isn't present or if the channel isn't a guild channel.
     *
     * @throws [RequestException] if something went wrong during the request.
     */
    override suspend fun asChannelOrNull(): GuildMessageChannel? =
            super<GuildChannelBehavior>.asChannelOrNull() as? GuildMessageChannel

    /**
     * Requests to bulk delete the [messages].
     * Messages older than 14 days will be deleted individually.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun bulkDelete(messages: Iterable<Snowflake>) {
        val daysLimit = Instant.now() - Duration.ofDays(14)
        //split up in bulk delete and manual delete
        // if message.timeMark + 14 days > now, then the message isn't 14 days old yet, and we can add it to the bulk delete
        // if message.timeMark + 14 days < now, then the message is more than 14 days old, and we'll have to manually delete them
        val (younger, older) = messages.partition { it.timeStamp.isAfter(daysLimit) }

        younger.map { it.value }.chunked(100).forEach {
            if (it.size < 2) kord.rest.channel.deleteMessage(id.value, it.first())
            else kord.rest.channel.bulkDelete(id.value, BulkDeleteRequest(it))
        }

        older.forEach { kord.rest.channel.deleteMessage(id.value, it.value) }
    }

    /**
     * Returns a new [GuildMessageChannelBehavior] with the given [strategy].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>): GuildMessageChannelBehavior = GuildMessageChannelBehavior(guildId, id, kord, strategy)

    companion object {
        internal operator fun invoke(guildId: Snowflake, id: Snowflake, kord: Kord, strategy: EntitySupplyStrategy<*> = kord.resources.defaultStrategy) = object : GuildMessageChannelBehavior {
            override val guildId: Snowflake = guildId
            override val id: Snowflake = id
            override val kord: Kord = kord
            override val supplier: EntitySupplier = strategy.supply(kord)

            override fun hashCode(): Int = Objects.hash(id, guildId)

            override fun equals(other: Any?): Boolean = when (other) {
                is GuildChannelBehavior -> other.id == id && other.guildId == guildId
                is ChannelBehavior -> other.id == id
                else -> false
            }
        }
    }
}

/**
 * Requests to create a new webhook configured by the [builder].
 *
 * @return The created [Webhook] with the [Webhook.token] field present.
 *
 * @throws [RestRequestException] if something went wrong during the request.
 */
@OptIn(ExperimentalContracts::class)
suspend inline fun GuildMessageChannelBehavior.createWebhook(builder: WebhookCreateBuilder.() -> Unit): Webhook {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    val response = kord.rest.webhook.createWebhook(id.value, builder)
    val data = WebhookData.from(response)

    return Webhook(data, kord)
}

package com.gitlab.kordlib.core.behavior.channel

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.cache.data.MessageData
import com.gitlab.kordlib.core.entity.Message
import com.gitlab.kordlib.core.entity.Strategizable
import com.gitlab.kordlib.core.entity.channel.MessageChannel
import com.gitlab.kordlib.core.exception.EntityNotFoundException
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.rest.builder.message.EmbedBuilder
import com.gitlab.kordlib.rest.builder.message.MessageCreateBuilder
import com.gitlab.kordlib.rest.request.RestRequestException
import com.gitlab.kordlib.rest.service.RestClient
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import java.time.Instant
import java.util.*
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.coroutines.coroutineContext
import kotlin.time.TimeMark
import kotlin.time.seconds

/**
 * The behavior of a Discord channel that can use messages.
 */
interface MessageChannelBehavior : ChannelBehavior, Strategizable {

    /**
     * Requests to get the this behavior as a [MessageChannel].
     *
     * @throws [RequestException] if something went wrong during the request.
     * @throws [EntityNotFoundException] if the channel wasn't present.
     * @throws [ClassCastException] if the channel isn't a guild message channel.
     */
    override suspend fun asChannel(): MessageChannel = super.asChannel() as MessageChannel

    /**
     * Requests to get this behavior as a [MessageChannel],
     * returns null if the channel isn't present or if the channel isn't a guild channel.
     *
     * @throws [RequestException] if something went wrong during the request.
     */
    override suspend fun asChannelOrNull(): MessageChannel? = super.asChannelOrNull() as? MessageChannel

    /**
     * Requests to get all messages in this channel.
     *
     * Messages retrieved by this function will be emitted in chronological order (oldest -> newest).
     * Unrestricted consumption of the returned [Flow] is a potentially performance intensive operation, it is thus recommended
     * to limit the amount of messages requested by using [Flow.take], [Flow.takeWhile] or other functions that limit the amount
     * of messages requested.
     *
     * ```kotlin
     *  channel.getMessagesBefore(newer.id).takeWhile { it.id > older.id }
     * ```
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    val messages: Flow<Message> get() = getMessagesAfter(Snowflake(0))

    /**
     * Requests to get the pinned messages in this channel.
     *
     * This property is not resolvable through cache and will always use the [RestClient] instead.
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    val pinnedMessages: Flow<Message>
        get() = flow {
            val responses = kord.rest.channel.getChannelPins(id.value)

            for (response in responses) {
                val data = MessageData.from(response)
                emit(Message(data, kord))
            }
        }

    /**
     * Requests to create a message with only a [MessageCreateBuilder.content].
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun createMessage(content: String): Message = createMessage { this.content = content }

    /**
     * Requests to delete a message in this channel.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun deleteMessage(id: Snowflake): Unit = kord.rest.channel.deleteMessage(this.id.value, id.value)

    /**
     * Requests to get all messages in this channel that were created **before** [messageId].
     *
     * Messages retrieved by this function will be emitted in reverse-chronological older (newest -> oldest).
     *
     * The flow may use paginated requests to supply messages, [limit] will limit the maximum number of messages
     * supplied and may optimize the batch size accordingly. A value of [Int.MAX_VALUE] means no limit.
     *
     * Unrestricted consumption of the returned [Flow] is a potentially performance intensive operation, it is thus recommended
     * to limit the amount of messages requested by using [Flow.take], [Flow.takeWhile] or other functions that limit the amount
     * of messages requested.
     *
     * ```kotlin
     *  channel.getMessagesBefore(newer.id).takeWhile { it.id > older.id }
     * ```
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     *
     * @throws IllegalArgumentException if a [limit] < 1 was supplied.
     */
    fun getMessagesBefore(messageId: Snowflake, limit: Int = Int.MAX_VALUE): Flow<Message> =
            supplier.getMessagesBefore(channelId = id, messageId = messageId, limit = limit)

    /**
     * Requests to get all messages in this channel that were created **after** [messageId].
     *
     * Messages retrieved by this function will be emitted in chronological older (oldest -> newest).
     *
     * The flow may use paginated requests to supply messages, [limit] will limit the maximum number of messages
     * supplied and may optimize the batch size accordingly. A value of [Int.MAX_VALUE] means no limit.
     *
     * Unrestricted consumption of the returned [Flow] is a potentially performance intensive operation, it is thus recommended
     * to limit the amount of messages requested by using [Flow.take], [Flow.takeWhile] or other functions that limit the amount
     * of messages requested.
     *
     * ```kotlin
     *  channel.getMessagesAfter(older.id).takeWhile { it.id < newer.id }
     * ```
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     *
     * @throws IllegalArgumentException if a [limit] < 1 was supplied.
     */
    fun getMessagesAfter(messageId: Snowflake, limit: Int = Int.MAX_VALUE): Flow<Message> =
            supplier.getMessagesAfter(channelId = id, messageId = messageId, limit = limit)

    /**
     * Requests to get messages around (both older and newer) the [messageId].
     *
     * Messages retrieved by this function will be emitted in chronological older (oldest -> newest).
     *
     * Unlike [getMessagesAfter] and [getMessagesBefore], this flow can return **a maximum of 100 messages**.
     * As such, the accepted range of [limit] is reduced to 1..100.
     *
     * Supplied messages will be equally distributed  before and after the [messageId].
     * The remaining message for an odd [limit] is undefined and may appear on either side.
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     *
     * @throws IllegalArgumentException if the [limit] is outside the range of 1..100.
     */
    fun getMessagesAround(messageId: Snowflake, limit: Int = 100): Flow<Message> =
            supplier.getMessagesAround(channelId = id, messageId = messageId, limit = 100)

    /**
     * Requests to get a message with the given [messageId].
     *
     * @throws RequestException if something went wrong during the request.
     * @throws EntityNotFoundException if the message is null.
     */
    suspend fun getMessage(messageId: Snowflake): Message = supplier.getMessage(id, messageId)

    /**
     * Requests to get a message with the given [messageId],
     * returns null if the message isn't present or is not part of this channel.
     *
     * @throws RequestException if something went wrong during the request.
     */
    suspend fun getMessageOrNull(messageId: Snowflake): Message? = supplier.getMessageOrNull(id, messageId)

    /**
     * Requests to trigger the typing indicator for the bot in this channel.
     * The typing status will persist for 10 seconds or until the bot creates a message in the channel.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun type() {
        kord.rest.channel.triggerTypingIndicator(id.value)
    }

    /**
     * Requests to trigger the typing indicator for the bot in this channel.
     * The typing status will persist until the [mark] is reached.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun typeUntil(mark: TimeMark) {
        while (mark.hasNotPassedNow()) {
            type()
            delay(8.seconds.toLongMilliseconds()) //bracing ourselves for some network delays
        }
    }

    /**
     * Requests to trigger the typing indicator for the bot in this channel.
     * The typing status will persist until the [instant] is reached.
     *
     * @throws [RestRequestException] if something went wrong during the request.
     */
    suspend fun typeUntil(instant: Instant) {
        while (instant.isBefore(Instant.now())) {
            type()
            delay(8.seconds.toLongMilliseconds()) //bracing ourselves for some network delays
        }
    }

    /**
     * Returns a new [MessageChannelBehavior] with the given [strategy].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>): MessageChannelBehavior = MessageChannelBehavior(id, kord, strategy)

    companion object {
        internal operator fun invoke(id: Snowflake, kord: Kord, strategy: EntitySupplyStrategy<*> = kord.resources.defaultStrategy) = object : MessageChannelBehavior {
            override val id: Snowflake = id
            override val kord: Kord = kord
            override val supplier: EntitySupplier = strategy.supply(kord)

            override fun hashCode(): Int = Objects.hash(id)

            override fun equals(other: Any?): Boolean = when (other) {
                is ChannelBehavior -> other.id == id
                else -> false
            }
        }
    }

}

/**
 * Requests to create a message configured by the [builder].
 *
 * @throws [RestRequestException] if something went wrong during the request.
 */
@OptIn(ExperimentalContracts::class)
suspend inline fun MessageChannelBehavior.createMessage(builder: MessageCreateBuilder.() -> Unit): Message {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    val response = kord.rest.channel.createMessage(id.value, builder)
    val data = MessageData.from(response)

    return Message(data, kord)
}

/**
 * Requests to create a message with only an [embed][MessageCreateBuilder.embed].
 *
 * @throws [RestRequestException] if something went wrong during the request.
 */
@OptIn(ExperimentalContracts::class)
suspend inline fun MessageChannelBehavior.createEmbed(block: EmbedBuilder.() -> Unit): Message {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return createMessage { embed(block) }
}

/**
 * Requests to trigger the typing indicator for the bot in this channel.
 * The typing status will be refreshed until the [block] has been completed.
 *
 * ```kotlin
 * channel.withTyping {
 *     delay(20.seconds.toLongMilliseconds()) //some very long task
 *     createMessage("done!")
 * }
 * ```
 *
 * @throws [RestRequestException] if something went wrong during the request.
 */
@OptIn(ExperimentalContracts::class)
suspend inline fun <T : MessageChannelBehavior> T.withTyping(block: T.() -> Unit) {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    var typing = true

    kord.launch(context = coroutineContext) {
        while (typing) {
            type()
            delay(8.seconds.toLongMilliseconds())
        }
    }

    try {
        block()
    } finally {
        typing = false
    }
}

package com.gitlab.kordlib.core.behavior.channel

import com.gitlab.kordlib.common.annotation.KordPreview
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.cache.data.ChannelData
import com.gitlab.kordlib.core.entity.channel.Channel
import com.gitlab.kordlib.core.entity.channel.NewsChannel
import com.gitlab.kordlib.core.exception.EntityNotFoundException
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.rest.builder.channel.NewsChannelModifyBuilder
import com.gitlab.kordlib.rest.request.RestRequestException
import com.gitlab.kordlib.rest.service.patchNewsChannel
import java.util.*
import com.gitlab.kordlib.common.entity.Permission
import com.gitlab.kordlib.rest.json.request.ChannelFollowRequest
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

/**
 * The behavior of a Discord News Channel associated to a guild.
 */
interface NewsChannelBehavior : GuildMessageChannelBehavior {

    /**
     * Requests to get the this behavior as a [NewsChannel].
     *
     * @throws [RequestException] if something went wrong during the request.
     * @throws [EntityNotFoundException] if the channel wasn't present.
     * @throws [ClassCastException] if the channel isn't a [NewsChannel].
     */
    override suspend fun asChannel(): NewsChannel = super.asChannel() as NewsChannel

    /**
     * Requests to get this behavior as a [NewsChannel],
     * returns null if the channel isn't present or if the channel isn't a news channel.
     *
     * @throws [RequestException] if something went wrong during the request.
     */
    override suspend fun asChannelOrNull(): NewsChannel? = super.asChannelOrNull() as? NewsChannel

    /**
     * Requests to follow this channel, publishing cross posted messages to the [target] channel.
     *
     * This call requires the bot to have the [Permission.ManageWebhooks] permission.
     *
     * @throws [RequestException] if something went wrong during the request.
     */
    @KordPreview
    suspend fun follow(target: Snowflake) {
        kord.rest.channel.followNewsChannel(id.value, ChannelFollowRequest(webhookChannelId = target.value))
    }

    /**
     * Returns a new [NewsChannelBehavior] with the given [strategy].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>): NewsChannelBehavior = NewsChannelBehavior(guildId, id, kord, strategy)

    companion object {
        internal operator fun invoke(guildId: Snowflake, id: Snowflake, kord: Kord, strategy: EntitySupplyStrategy<*> = kord.resources.defaultStrategy): NewsChannelBehavior = object : NewsChannelBehavior {
            override val guildId: Snowflake = guildId
            override val id: Snowflake = id
            override val kord: Kord = kord
            override val supplier: EntitySupplier = strategy.supply(kord)

            override fun hashCode(): Int = Objects.hash(id, guildId)

            override fun equals(other: Any?): Boolean = when(other) {
                is GuildChannelBehavior -> other.id == id && other.guildId == guildId
                is ChannelBehavior -> other.id == id
                else -> false
            }
        }
    }

}

/**
 * Requests to edit this channel.
 *
 * @return The edited [NewsChannel].
 *
 * @throws [RestRequestException] if something went wrong during the request.
 */
@OptIn(ExperimentalContracts::class)
suspend inline fun NewsChannelBehavior.edit(builder: NewsChannelModifyBuilder.() -> Unit): NewsChannel {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    val response = kord.rest.channel.patchNewsChannel(id.value, builder)
    val data = ChannelData.from(response)

    return Channel.from(data, kord) as NewsChannel
}

package com.gitlab.kordlib.core.behavior.channel

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.cache.data.ChannelData
import com.gitlab.kordlib.core.entity.channel.Channel
import com.gitlab.kordlib.core.entity.channel.StoreChannel
import com.gitlab.kordlib.core.exception.EntityNotFoundException
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.rest.builder.channel.StoreChannelModifyBuilder
import com.gitlab.kordlib.rest.request.RestRequestException
import com.gitlab.kordlib.rest.service.patchStoreChannel
import java.util.*

/**
 * The behavior of a Discord Store Channel associated to a guild.
 */
interface StoreChannelBehavior : GuildChannelBehavior {

    /**
     * Requests to get the this behavior as a [StoreChannel].
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the channel wasn't present.
     * @throws [ClassCastException] if the channel isn't a [StoreChannel].
     */
    override suspend fun asChannel(): StoreChannel = super.asChannel() as StoreChannel

    /**
     * Requests to get this behavior as a [StoreChannel],
     * returns null if the channel isn't present or if the channel isn't a [StoreChannel].
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    override suspend fun asChannelOrNull(): StoreChannel? = super.asChannelOrNull() as? StoreChannel

    /**
     * returns a new [StoreChannelBehavior] with the given [strategy].
     *
     * @param strategy the strategy to use for the new instance. By default [EntitySupplyStrategy.CacheWithRestFallback].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>): StoreChannelBehavior = StoreChannelBehavior(guildId, id, kord, strategy)

    companion object {
        internal operator fun invoke(guildId: Snowflake, id: Snowflake, kord: Kord, strategy: EntitySupplyStrategy<*> = kord.resources.defaultStrategy): StoreChannelBehavior = object : StoreChannelBehavior {
            override val guildId: Snowflake = guildId
            override val id: Snowflake = id
            override val kord: Kord = kord
            override val supplier: EntitySupplier = strategy.supply(kord)

            override fun hashCode(): Int = Objects.hash(id, guildId)

            override fun equals(other: Any?): Boolean = when(other) {
                is GuildChannelBehavior -> other.id == id && other.guildId == guildId
                is ChannelBehavior -> other.id == id
                else -> false
            }
        }
    }

}

/**
 * Requests to edit this channel.
 *
 * @return The edited [StoreChannel].
 *
 * @throws [RestRequestException] if something went wrong during the request.
 */
suspend inline fun StoreChannelBehavior.edit(builder: StoreChannelModifyBuilder.() -> Unit): StoreChannel {
    val response = kord.rest.channel.patchStoreChannel(id.value, builder)
    val data = ChannelData.from(response)

    return Channel.from(data, kord) as StoreChannel
}

package com.gitlab.kordlib.core.behavior.channel

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.cache.data.ChannelData
import com.gitlab.kordlib.core.entity.channel.Channel
import com.gitlab.kordlib.core.entity.channel.TextChannel
import com.gitlab.kordlib.core.exception.EntityNotFoundException
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.rest.builder.channel.TextChannelModifyBuilder
import com.gitlab.kordlib.rest.request.RestRequestException
import com.gitlab.kordlib.rest.service.patchTextChannel
import java.util.*

interface TextChannelBehavior : GuildMessageChannelBehavior {

    /**
     * Requests to get the this behavior as a [TextChannel].
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the channel wasn't present.
     * @throws [ClassCastException] if the channel isn't a [TextChannel].
     */
    override suspend fun asChannel(): TextChannel = super.asChannel() as TextChannel

    /**
     * Requests to get this behavior as a [TextChannel],
     * returns null if the channel isn't present or if the channel isn't a [TextChannel].
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    override suspend fun asChannelOrNull(): TextChannel? = super.asChannelOrNull() as? TextChannel

    /**
     * Returns a new [TextChannelBehavior] with the given [strategy].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>): TextChannelBehavior = TextChannelBehavior(guildId, id, kord, strategy)

    companion object {
        internal operator fun invoke(
                guildId: Snowflake,
                id: Snowflake,
                kord: Kord,
                strategy: EntitySupplyStrategy<*> = kord.resources.defaultStrategy
        ): TextChannelBehavior = object : TextChannelBehavior {
            override val guildId: Snowflake = guildId
            override val id: Snowflake = id
            override val kord: Kord = kord
            override val supplier: EntitySupplier = strategy.supply(kord)

            override fun hashCode(): Int = Objects.hash(id, guildId)

            override fun equals(other: Any?): Boolean = when(other) {
                is GuildChannelBehavior -> other.id == id && other.guildId == guildId
                is ChannelBehavior -> other.id == id
                else -> false
            }
        }
    }

}

/**
 * Requests to edit this channel.
 *
 * @return The edited [TextChannel].
 *
 * @throws [RestRequestException] if something went wrong during the request.
 */
suspend inline fun TextChannelBehavior.edit(builder: TextChannelModifyBuilder.() -> Unit): TextChannel {
    val response = kord.rest.channel.patchTextChannel(id.value, builder)

    val data = ChannelData.from(response)
    return Channel.from(data, kord) as TextChannel
}

package com.gitlab.kordlib.core.behavior.channel

import com.gitlab.kordlib.cache.api.query
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.cache.data.ChannelData
import com.gitlab.kordlib.core.cache.data.VoiceStateData
import com.gitlab.kordlib.core.entity.VoiceState
import com.gitlab.kordlib.core.entity.channel.Channel
import com.gitlab.kordlib.core.entity.channel.VoiceChannel
import com.gitlab.kordlib.core.exception.EntityNotFoundException
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.rest.builder.channel.VoiceChannelModifyBuilder
import com.gitlab.kordlib.rest.request.RestRequestException
import com.gitlab.kordlib.rest.service.patchVoiceChannel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import java.util.*
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

/**
 * The behavior of a Discord Voice Channel associated to a guild.
 */
interface VoiceChannelBehavior : GuildChannelBehavior {

    /**
     * Requests to retrieve the present voice states of this channel.
     *
     * This property is not resolvable through REST and will always use [KordCache] instead.
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    val voiceStates: Flow<VoiceState>
        get() = kord.cache.query<VoiceStateData> { VoiceStateData::channelId eq id.longValue }
                .asFlow()
                .map { VoiceState(it, kord) }

    /**
     * Requests to get the this behavior as a [VoiceChannel].
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the channel wasn't present.
     * @throws [ClassCastException] if the channel isn't a [VoiceChannel].
     */
    override suspend fun asChannel(): VoiceChannel = super.asChannel() as VoiceChannel

    /**
     * Requests to get this behavior as a [VoiceChannel],
     * returns null if the channel isn't present or if the channel isn't a [VoiceChannel].
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    override suspend fun asChannelOrNull(): VoiceChannel? = super.asChannelOrNull() as? VoiceChannel

    /**
     * Returns a new [VoiceChannelBehavior] with the given [strategy].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>): VoiceChannelBehavior = VoiceChannelBehavior(guildId, id, kord, strategy)

    companion object {
        internal operator fun invoke(guildId: Snowflake, id: Snowflake, kord: Kord, strategy: EntitySupplyStrategy<*> = kord.resources.defaultStrategy) = object : VoiceChannelBehavior {
            override val guildId: Snowflake = guildId
            override val id: Snowflake = id
            override val kord: Kord = kord
            override val supplier: EntitySupplier = strategy.supply(kord)

            override fun hashCode(): Int = Objects.hash(id, guildId)

            override fun equals(other: Any?): Boolean = when(other) {
                is GuildChannelBehavior -> other.id == id && other.guildId == guildId
                is ChannelBehavior -> other.id == id
                else -> false
            }
        }
    }

}

/**
 * Requests to edit this channel.
 *
 * @return The edited [VoiceChannel].
 *
 * @throws [RestRequestException] if something went wrong during the request.
 */
@OptIn(ExperimentalContracts::class)
suspend inline fun VoiceChannelBehavior.edit(builder: VoiceChannelModifyBuilder.() -> Unit): VoiceChannel {
    contract {
        callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
    }
    val response = kord.rest.channel.patchVoiceChannel(id.value, builder)

    val data = ChannelData.from(response)
    return Channel.from(data, kord) as VoiceChannel
}

@file:Suppress("EXPERIMENTAL_API_USAGE")

package com.gitlab.kordlib.core.builder.kord

import com.gitlab.kordlib.cache.api.DataCache
import com.gitlab.kordlib.common.ratelimit.BucketRateLimiter
import com.gitlab.kordlib.core.ClientResources
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.cache.CachingGateway
import com.gitlab.kordlib.core.cache.KordCacheBuilder
import com.gitlab.kordlib.core.cache.createView
import com.gitlab.kordlib.core.cache.registerKordData
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.exception.KordInitializationException
import com.gitlab.kordlib.core.gateway.MasterGateway
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.gateway.DefaultGateway
import com.gitlab.kordlib.gateway.DefaultGatewayData
import com.gitlab.kordlib.gateway.Gateway
import com.gitlab.kordlib.gateway.Intents
import com.gitlab.kordlib.gateway.retry.LinearRetry
import com.gitlab.kordlib.gateway.retry.Retry
import com.gitlab.kordlib.rest.json.response.BotGatewayResponse
import com.gitlab.kordlib.rest.ratelimit.ExclusionRequestRateLimiter
import com.gitlab.kordlib.rest.request.KtorRequestHandler
import com.gitlab.kordlib.rest.request.RequestHandler
import com.gitlab.kordlib.rest.request.isError
import com.gitlab.kordlib.rest.route.Route
import com.gitlab.kordlib.rest.service.RestClient
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.readText
import io.ktor.http.HttpStatusCode
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.parse
import mu.KotlinLogging
import kotlin.concurrent.thread
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.time.seconds

operator fun DefaultGateway.Companion.invoke(resources: ClientResources, retry: Retry = LinearRetry(2.seconds, 60.seconds, 10)) =
        DefaultGateway(DefaultGatewayData("wss://gateway.discord.gg/", resources.httpClient, retry, BucketRateLimiter(120, 60.seconds), BucketRateLimiter(1, 5.seconds)))

private val logger = KotlinLogging.logger { }

class KordBuilder(val token: String) {
    private var shardRange: (recommended: Int) -> Iterable<Int> = { 0 until it }
    private var gatewayBuilder: (resources: ClientResources, shards: List<Int>) -> List<Gateway> = { resources, shards ->
        val rateLimiter = BucketRateLimiter(1, 5.seconds)
        shards.map {
            DefaultGateway {
                client = resources.httpClient
                identifyRateLimiter = rateLimiter
            }
        }
    }

    private var handlerBuilder: (resources: ClientResources) -> RequestHandler =
            { KtorRequestHandler(it.httpClient, ExclusionRequestRateLimiter()) }
    private var cacheBuilder: KordCacheBuilder.(resources: ClientResources) -> Unit = {}

    /**
     * Enable adding a [Runtime.addShutdownHook] to log out of the [Gateway] when the process is killed.
     */
    var enableShutdownHook: Boolean = true

    /**
     * The [CoroutineDispatcher] kord uses to launch suspending tasks. [Dispatchers.Default] by default.
     */
    var defaultDispatcher: CoroutineDispatcher = Dispatchers.Default

    /**
     * The default strategy used by entities to retrieve entities. [EntitySupplyStrategy.cacheWithRestFallback] by default.
     */
    var defaultStrategy: EntitySupplyStrategy<*> = EntitySupplyStrategy.cacheWithRestFallback

    /**
     * The client used for building [Gateways][Gateway] and [RequestHandlers][RequestHandler]. A default implementation
     * will be used when not set.
     */
    var httpClient: HttpClient? = null

    /**
     * The enabled gateway intents, setting intents to null will disable the feature.
     */
    var intents: Intents? = null

    /**
     * Configures the shards this client will connect to, by default `0 until recommended`.
     * This can be used to break up to client into multiple processes.
     *
     * ```
     * cache {
     *  defaultGenerator = lruCache(10)
     *  forDescription(UserData.description) { cache, description ->  DataEntryCache.none() }
     *  forDescription(MessageData.description) { cache, description ->
     *      MapEntryCache(cache, description, MapLikeCollection.lruLinkedHashMap(100))
     *  }
     *  forDescription(UserData.description) { cache, description ->
     *      MapEntryCache(cache, description, MapLikeCollection.weakHashMap())
     *  }
     *}
     * ```
     */
    fun sharding(shardRange: (recommended: Int) -> Iterable<Int>) {
        this.shardRange = shardRange
    }

    /**
     * Configures the [Gateway] for each shard.
     *
     * ```
     * Kord(token) {
     *     gateway { resources, shards ->
     *         shards.map { DefaultGateway(resources) }
     *     }
     * }
     * ```
     */
    fun gateways(gatewayBuilder: (resources: ClientResources, shards: List<Int>) -> List<Gateway>) {
        this.gatewayBuilder = gatewayBuilder
    }

    /**
     * Configures the enabled intents across all gateways.
     *
     * ```kotlin
     * intents {
     *     +Intent.DirectMessages
     *
     *     +Intents.nonPrivileged
     *
     *     enableEvent<MessageCreateEvent>()
     *
     *     enableEvents(TypingStartEvent::class, MessageDeleteEvent::class)
     * }
     * ```
     */
    @OptIn(ExperimentalContracts::class)
    inline fun intents(builder: Intents.IntentsBuilder.() -> Unit) {
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }
        intents = Intents { builder() }
    }

    /**
     * Configures the [RequestHandler] for the [RestClient].
     *
     * ```
     * Kord(token) {
     *     { resources -> KtorRequestHandler(resources.httpClient, ExclusionRequestRateLimiter()) }
     * }
     * ```
     */
    fun requestHandler(handlerBuilder: (resources: ClientResources) -> RequestHandler) {
        this.handlerBuilder = handlerBuilder
    }

    /**
     * Configures the [DataCache] for caching.
     *
     *  ```
     * Kord(token) {
     *     cache {
     *         defaultGenerator = lruCache()
     *         forDescription(MessageData.description) { cache, description -> DataEntryCache.none() }
     *         forDescription(UserData.description) { cache, description -> MapEntryCache(cache, description, MapLikeCollection.weakHashMap()) }
     *     }
     * }
     * ```
     */
    fun cache(builder: KordCacheBuilder.(resources: ClientResources) -> Unit) {
        val old = cacheBuilder
        cacheBuilder = { resources: ClientResources ->
            old(resources)
            builder(resources)
        }
    }

    /**
     * Requests the gateway info for the bot, or throws a [KordInitializationException] when something went wrong.
     */
    private suspend fun HttpClient.getGatewayInfo(): BotGatewayResponse {
        val response = get<HttpResponse>("${Route.baseUrl}${Route.GatewayBotGet.path}")
        val responseBody = response.readText()
        if (response.isError) {
            val message = buildString {
                append("Something went wrong while initializing Kord.")
                if (response.status == HttpStatusCode.Unauthorized) {
                    append(", make sure the bot token you entered is valid.")
                }

                appendln(responseBody)
            }

            throw KordInitializationException(message)
        }

        return Json { ignoreUnknownKeys = true }.decodeFromString(BotGatewayResponse.serializer(), responseBody)
    }

    /**
     * @throws KordInitializationException if something went wrong while getting the bot's gateway information.
     */
    suspend fun build(): Kord {
        val client = httpClient.configure(token)

        val recommendedShards = client.getGatewayInfo().shards
        val shards = shardRange(recommendedShards).toList()

        if (client.engine.config.threadsCount < shards.size + 1) {
            logger.warn {
                """
                kord's http client is currently using ${client.engine.config.threadsCount} threads,
                which is less than the advised threadcount of ${shards.size + 1} (number of shards + 1)""".trimIndent()
            }
        }

        val resources = ClientResources(token, shards.count(), client, defaultStrategy, intents)
        val rest = RestClient(handlerBuilder(resources))
        val cache = KordCacheBuilder().apply { cacheBuilder(resources) }.build()
        cache.registerKordData()
        val gateway = run {
            val gateways = buildMap<Int, Gateway> {
                val gateways = gatewayBuilder(resources, shards)
                        .map { CachingGateway(cache.createView(), it) }
                        .onEach { it.registerKordData() }

                shards.forEachIndexed { index, shard ->
                    put(shard, gateways[index])
                }
            }
            MasterGateway(gateways)
        }

        val self = getBotIdFromToken(token)

        val eventPublisher = BroadcastChannel<Event>(1)

        if (enableShutdownHook) {
            Runtime.getRuntime().addShutdownHook(thread(false) {
                runBlocking {
                    gateway.detachAll()
                }
            })
        }

        return Kord(
                resources,
                cache,
                gateway,
                rest,
                self,
                eventPublisher,
                defaultDispatcher
        )
    }

}

package com.gitlab.kordlib.core.builder.kord

import com.gitlab.kordlib.common.entity.Snowflake
import io.ktor.client.HttpClient
import io.ktor.client.HttpClientConfig
import io.ktor.client.engine.cio.CIO
import io.ktor.client.features.defaultRequest
import io.ktor.client.features.json.JsonFeature
import io.ktor.client.features.json.serializer.KotlinxSerializer
import io.ktor.client.features.websocket.WebSockets
import io.ktor.client.request.header
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import java.util.*

internal fun HttpClientConfig<*>.defaultConfig(token: String) {
    expectSuccess = false
    defaultRequest {
        header("Authorization", "Bot $token")
    }

    install(JsonFeature)
    install(WebSockets)
}

internal fun HttpClient?.configure(token: String): HttpClient {
    if (this != null) return this.config {
        defaultConfig(token)
    }

    val json = Json {
        encodeDefaults = false
        allowStructuredMapKeys = true
        ignoreUnknownKeys = true
        isLenient = true
    }

    return HttpClient(CIO) {
        defaultConfig(token)
        install(JsonFeature) {
            serializer = KotlinxSerializer(json)
        }
    }
}

internal fun getBotIdFromToken(token: String): Snowflake {
    try {
        val bytes = Base64.getDecoder().decode(token.split(""".""").first())
        return Snowflake(String(bytes))
    } catch (exception: IllegalArgumentException) {
        throw IllegalArgumentException("Malformed bot token: '$token'. Make sure that your token is correct.")
    }
}

package com.gitlab.kordlib.core.builder.kord

import com.gitlab.kordlib.cache.api.DataCache
import com.gitlab.kordlib.common.annotation.KordExperimental
import com.gitlab.kordlib.core.ClientResources
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.exception.KordInitializationException
import com.gitlab.kordlib.core.gateway.MasterGateway
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.gateway.Gateway
import com.gitlab.kordlib.gateway.Intents
import com.gitlab.kordlib.rest.ratelimit.ExclusionRequestRateLimiter
import com.gitlab.kordlib.rest.request.KtorRequestHandler
import com.gitlab.kordlib.rest.request.RequestHandler
import com.gitlab.kordlib.rest.service.RestClient
import io.ktor.client.HttpClient
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.channels.Channel

@KordExperimental
class KordRestOnlyBuilder(val token: String) {

    private var handlerBuilder: (resources: ClientResources) -> RequestHandler =
            { KtorRequestHandler(it.httpClient, ExclusionRequestRateLimiter()) }

    /**
     * The [CoroutineDispatcher] kord uses to launch suspending tasks. [Dispatchers.Default] by default.
     */
    var defaultDispatcher: CoroutineDispatcher = Dispatchers.Default

    /**
     * The client used for building [Gateways][Gateway] and [RequestHandlers][RequestHandler]. A default implementation
     * will be used when not set.
     */
    var httpClient: HttpClient? = null

    /**
     * Configures the [RequestHandler] for the [RestClient].
     *
     * ```
     * Kord(token) {
     *     { resources -> KtorRequestHandler(resources.httpClient, ExclusionRequestRateLimiter()) }
     * }
     * ```
     */
    fun requestHandler(handlerBuilder: (resources: ClientResources) -> RequestHandler) {
        this.handlerBuilder = handlerBuilder
    }

    /**
     * @throws KordInitializationException if something went wrong while getting the bot's gateway information.
     */
    fun build(): Kord {
        val client = httpClient.configure(token)

        val resources = ClientResources(token, 0, client, EntitySupplyStrategy.rest, Intents.none)
        val rest = RestClient(handlerBuilder(resources))
        val selfId = getBotIdFromToken(token)

        val eventPublisher = BroadcastChannel<Event>(1)

        return Kord(
                resources,
                DataCache.none(),
                MasterGateway(mapOf(0 to Gateway.none())),
                rest,
                selfId,
                eventPublisher,
                defaultDispatcher
        )
    }
}

package com.gitlab.kordlib.core.cache.data

import com.gitlab.kordlib.common.entity.ActivityType
import com.gitlab.kordlib.common.entity.DiscordActivity
import com.gitlab.kordlib.common.entity.DiscordPartialEmoji
import kotlinx.serialization.Serializable

@Serializable
data class ActivityData(
        val name: String,
        val type: ActivityType,
        val url: String? = null,
        val start: Long? = null,
        val stop: Long? = null,
        val applicationId: String? = null,
        val details: String? = null,
        val emoji: DiscordPartialEmoji? = null,
        val state: String? = null,
        val partyId: String? = null,
        val partyCurrentSize: Int? = null,
        val partyMaxSize: Int? = null,
        val largeImage: String? = null,
        val largeText: String? = null,
        val smallImage: String? = null,
        val smallText: String? = null,
        val secretsJoin: String? = null,
        val secretsSpectate: String? = null,
        val secretsMatch: String? = null,
        val instance: Boolean? = null,
        val flags: Int? = null
) {
    companion object {
        fun from(entity: DiscordActivity) = with(entity) {
            ActivityData(
                    name,
                    type,
                    url,
                    timestamps?.start,
                    timestamps?.end,
                    applicationId,
                    details,
                    emoji,
                    state,
                    party?.id,
                    party?.size?.first(),
                    party?.size?.last(),
                    assets?.largeImage,
                    assets?.largeText,
                    assets?.smallImage,
                    assets?.smallText,
                    secrets?.join,
                    secrets?.spectate,
                    secrets?.match,
                    instance,
                    flags
            )
        }
    }

}

package com.gitlab.kordlib.core.cache.data

import com.gitlab.kordlib.rest.json.response.ApplicationInfoResponse

data class ApplicationInfoData(
        val id: Long,
        val name: String,
        val icon: String?,
        val description: String?,
        val botPublic: Boolean,
        val botRequireCodeGrant: Boolean,
        val ownerId: Long,
        val summary: String,
        val verifyKey: String,
        val teamId: Long?,
        val guildId: Long? = null,
        val primarySkuId: Long? = null,
        val slug: String? = null,
        val coverImage: String? = null
) {
    companion object {

        fun from(entity: ApplicationInfoResponse) = with(entity) {
            ApplicationInfoData(
                    id.toLong(),
                    name,
                    icon,
                    description,
                    botPublic,
                    botRequireCodeGrant,
                    owner.id.toLong(),
                    summary,
                    verifyKey,
                    team?.id?.toLong(),
                    guildId?.toLong(),
                    primarySkuId?.toLong(),
                    slug,
                    coverImage
            )
        }

    }
}

package com.gitlab.kordlib.core.cache.data

import com.gitlab.kordlib.common.entity.Attachment
import kotlinx.serialization.Serializable

@Serializable
data class AttachmentData(
        val id: Long,
        val filename: String,
        val size: Int,
        val url: String,
        val proxyUrl: String,
        val height: Int? = null,
        val width: Int? = null
) {
    companion object {
        fun from(entity: Attachment) = with(entity) {
            AttachmentData(id.toLong(), filename ?: "", size, url, proxyUrl, height, width)
        }
    }
}

package com.gitlab.kordlib.core.cache.data

import com.gitlab.kordlib.rest.json.response.BanResponse
import kotlinx.serialization.Serializable

@Serializable
data class BanData(val reason: String?, val userId: Long, val guildId: Long) {
    companion object {
        fun from(guildId: String, entity: BanResponse) = with(entity) {
            BanData(reason, user.id.toLong(), guildId.toLong())
        }
    }
}

package com.gitlab.kordlib.core.cache.data

import com.gitlab.kordlib.cache.api.data.description
import com.gitlab.kordlib.common.entity.ChannelType
import com.gitlab.kordlib.common.entity.DiscordChannel
import kotlinx.serialization.Serializable

@Serializable
data class ChannelData(
        val id: Long,
        val type: ChannelType,
        val guildId: Long? = null,
        val position: Int? = null,
        val permissionOverwrites: List<PermissionOverwriteData> = emptyList(),
        val name: String? = null,
        val topic: String? = null,
        val nsfw: Boolean? = null,
        val lastMessageId: Long? = null,
        val bitrate: Int? = null,
        val userLimit: Int? = null,
        val rateLimitPerUser: Int? = null,
        val recipients: List<Long>? = null,
        val icon: String? = null,
        val ownerId: Long? = null,
        val applicationId: Long? = null,
        val parentId: Long? = null,
        val lastPinTimestamp: String? = null
) {

    companion object {
        val description = description(ChannelData::id)

        fun from(entity: DiscordChannel) = with(entity) {
            ChannelData(
                    id.toLong(),
                    type,
                    guildId?.toLong(),
                    position,
                    permissionOverwrites.orEmpty().map { PermissionOverwriteData.from(it) },
                    name,
                    topic,
                    nsfw,
                    lastMessageId?.toLong(),
                    bitrate,
                    userLimit,
                    rateLimitPerUser,
                    recipients?.map { it.id.toLong() },
                    icon,
                    ownerId?.toLong(),
                    applicationId?.toLong(),
                    parentId?.toLong(),
                    lastPinTimestamp
            )
        }
    }

}

fun DiscordChannel.toData() = ChannelData.from(this)

package com.gitlab.kordlib.core.cache.data

import com.gitlab.kordlib.common.entity.DiscordClientStatus
import com.gitlab.kordlib.common.entity.Status
import kotlinx.serialization.Serializable

@Serializable
data class ClientStatusData(
        val desktop: Status?,
        val mobile: Status?,
        val web: Status?
) {
    companion object {
        fun from(entity: DiscordClientStatus) = with(entity) {
            ClientStatusData(desktop, mobile, web)
        }
    }
}

package com.gitlab.kordlib.core.cache.data

import com.gitlab.kordlib.common.entity.Embed
import kotlinx.serialization.Serializable

@Serializable
data class EmbedData(
        val title: String? = null,
        val type: String? = null,
        val description: String? = null,
        val url: String? = null,
        val timestamp: String? = null,
        val color: Int? = null,
        val footer: EmbedFooterData? = null,
        val image: EmbedImageData? = null,
        val thumbnail: EmbedThumbnailData? = null,
        val video: EmbedVideoData? = null,
        val provider: EmbedProviderData? = null,
        val author: EmbedAuthorData? = null,
        val fields: List<EmbedFieldData> = emptyList()
) {
    companion object {
        fun from(entity: Embed) = with(entity) {
            EmbedData(
                    title,
                    type,
                    description,
                    url,
                    timestamp,
                    color,
                    footer?.let { EmbedFooterData.from(it) },
                    image?.let { EmbedImageData.from(it) },
                    thumbnail?.let { EmbedThumbnailData.from(it) },
                    video?.let { EmbedVideoData.from(it) },
                    provider?.let { EmbedProviderData.from(it) },
                    author?.let { EmbedAuthorData.from(it) },
                    fields.orEmpty().map { EmbedFieldData.from(it) }
            )
        }
    }
}

@Serializable
data class EmbedFooterData(
        val text: String,
        val iconUrl: String? = null,
        val proxyIconUrl: String? = null
) {
    companion object {
        fun from(entity: Embed.Footer) = with(entity) {
            EmbedFooterData(text, iconUrl, proxyIconUrl)
        }
    }
}

@Serializable
data class EmbedImageData(
        val url: String? = null,
        val proxyUrl: String? = null,
        val height: Int? = null,
        val width: Int? = null
) {
    companion object {
        fun from(entity: Embed.Image) = with(entity) {
            EmbedImageData(url, proxyUrl, height, width)
        }
    }
}

@Serializable
data class EmbedThumbnailData(
        val url: String? = null,
        val proxyUrl: String? = null,
        val height: Int? = null,
        val width: Int? = null
) {
    companion object {
        fun from(entity: Embed.Thumbnail) = with(entity) {
            EmbedThumbnailData(url, proxyUrl, height, width)
        }
    }
}

@Serializable
data class EmbedVideoData(
        val url: String? = null,
        val height: Int? = null,
        val width: Int? = null
) {
    companion object {
        fun from(entity: Embed.Video) = with(entity) {
            EmbedVideoData(url, height, width)
        }
    }
}

@Serializable
data class EmbedProviderData(
        val name: String? = null,
        val url: String? = null
) {
    companion object {
        fun from(entity: Embed.Provider) = with(entity) {
            EmbedProviderData(name, url)
        }
    }
}

@Serializable
data class EmbedAuthorData(
        val name: String? = null,
        val url: String? = null,
        val iconUrl: String? = null,
        val proxyIconUrl: String? = null
) {
    companion object {
        fun from(entity: Embed.Author) = with(entity) {
            EmbedAuthorData(name, url, iconUrl, proxyIconUrl)
        }
    }
}

@Serializable
data class EmbedFieldData(
        val name: String,
        val value: String,
        val inline: Boolean? = null
) {
    companion object {
        fun from(entity: Embed.Field) = with(entity) {
            EmbedFieldData(name, value, inline)
        }
    }
}

package com.gitlab.kordlib.core.cache.data

import com.gitlab.kordlib.cache.api.data.description
import com.gitlab.kordlib.common.entity.DiscordEmoji
import kotlinx.serialization.Serializable

@Serializable
data class EmojiData(
        val id: Long,
        val guildId: Long,
        val name: String? = null,
        val user: UserData?,
        val roles: List<Long>,
        val requireColons: Boolean,
        val managed: Boolean,
        val animated: Boolean,
        val available: Boolean
) {
    companion object {
        val description = description(EmojiData::id)

        fun from(guildId: String, id: String, entity: DiscordEmoji) =
                with(entity) {
                    EmojiData(
                            id.toLong(),
                            guildId.toLong(),
                            name,
                            user?.let { UserData.from(it) },
                            roles.orEmpty().map { it.toLong() },
                            requireColons ?: false,
                            managed ?: false,
                            animated ?: false,
                            available ?: false
                    )
                }
    }
}

package com.gitlab.kordlib.core.cache.data

import com.gitlab.kordlib.cache.api.data.description
import com.gitlab.kordlib.common.entity.*
import kotlinx.serialization.Serializable

@Serializable
data class GuildData(
        val id: Long,
        val name: String,
        val icon: String? = null,
        val splash: String? = null,
        val owner: Boolean? = null,
        val ownerId: Long,
        val permissions: Permissions? = null,
        val region: String,
        val afkChannelId: Long? = null,
        val afkTimeout: Int,
        //TODO, keep this?
        val embedEnabled: Boolean? = null,
        val embedChannelId: Long? = null,
        val verificationLevel: VerificationLevel,
        val defaultMessageNotifications: DefaultMessageNotificationLevel,
        val explicitContentFilter: ExplicitContentFilter,
        val roles: List<Long>,
        val emojis: List<EmojiData>,
        val features: List<GuildFeature>,
        val mfaLevel: MFALevel,
        val applicationId: Long? = null,
        val widgetEnabled: Boolean? = null,
        val widgetChannelId: Long? = null,
        val systemChannelId: Long? = null,
        val joinedAt: String? = null,
        val large: Boolean? = null,
        val memberCount: Int? = null,
        val voiceStates: List<VoiceStateData> = emptyList(),
        val members: List<MemberData> = emptyList(),
        val channels: List<Long> = emptyList(),
        val presences: List<PresenceData> = emptyList(),
        val maxPresences: Int? = null,
        val maxMembers: Int? = null,
        val vanityUrlCode: String? = null,
        val description: String? = null,
        val banner: String? = null,
        val systemChannelFlags: SystemChannelFlags? = null,
        val rulesChannelId: Long? = null,
        val discoverySplash: String? = null,
        val publicUpdatesChannelId: Long? = null,
        val preferredLocale: String,

        /**
         * Approximate number of members in this guild,
         * returned from the GET /guild/<id> endpoint when with_counts is true
         */
        val approximateMemberCount: Int? = null,

        /**
         * Approximate number of online members in this guild,
         * returned from the GET /guild/<id> endpoint when with_counts is true
         */
        val approximatePresenceCount: Int? = null
) {
    companion object {

        val description = description(GuildData::id) {
            link(GuildData::id to RoleData::guildId)
            link(GuildData::id to ChannelData::guildId)
            link(GuildData::id to MemberData::guildId)
            link(GuildData::id to MessageData::guildId)
            link(GuildData::id to WebhookData::guildId)
            link(GuildData::id to VoiceStateData::guildId)
            link(GuildData::id to PresenceData::guildId)
        }

        fun from(entity: DiscordGuild) = with(entity) {
            GuildData(
                    id.toLong(),
                    name,
                    icon,
                    splash,
                    owner,
                    ownerId.toLong(),
                    permissions,
                    region,
                    afkChannelId?.toLong(),
                    afkTimeout,
                    embedEnabled,
                    embedChannelId?.toLong(),
                    verificationLevel,
                    defaultMessageNotifications,
                    explicitContentFilter,
                    roles.map { it.id.toLong() },
                    emojis.map { EmojiData.from(guildId = id, id = it.id!!, entity = it) },
                    features,
                    mfaLevel,
                    applicationId?.toLong(),
                    widgetEnabled,
                    widgetChannelId?.toLong(),
                    systemChannelId?.toLong(),
                    joinedAt,
                    large,
                    memberCount,
                    voiceStates.orEmpty().map { VoiceStateData.from(id, it) },
                    members.orEmpty().map { MemberData.from(userId = it.user!!.id, guildId = id, entity = it) },
                    channels.orEmpty().map { it.id.toLong() },
                    presences.orEmpty().map { PresenceData.from(id, it) },
                    maxPresences,
                    maxMembers,
                    vanityUrlCode,
                    description,
                    banner,
                    systemChannelFlags,
                    rulesChannelId?.toLong(),
                    discoverySplash,
                    publicUpdatesChannelId?.toLong(),
                    preferredLocale,
                    approximateMemberCount = approximateMemberCount,
                    approximatePresenceCount = approximatePresenceCount
            )
        }
    }
}

fun DiscordGuild.toData() = GuildData.from(this)

package com.gitlab.kordlib.core.cache.data

import com.gitlab.kordlib.common.entity.DiscordGuildPreview
import com.gitlab.kordlib.common.entity.GuildFeature
import kotlinx.serialization.Serializable

@Serializable
class GuildPreviewData(
        /**
         * Guild id.
         */
        val id: Long,
        /**
         * Guild name (2-100) characters.
         */
        val name: String,

        /**
         * Icon hash.
         */
        val icon: String? = null,

        /**
         * Splash hash.
         */
        val splash: String? = null,

        /**
         * Discovery splash hash.
         */
        val discoverySplash: String? = null,

        /**
         * Ids of custom guild emojis.
         */
        val emojis: List<EmojiData>,

        /**
         * Enabled guild features.
         */
        val features: List<GuildFeature>,

        /**
         * Approximate number of members in this guild.
         */
        val approximateMemberCount: Int,

        /**
         * Approximate number of online members in this guild.
         */
        val approximatePresenceCount: Int,

        /**
         * The description for the guild.
         */
        val description: String? = null
) {
        companion object {
                fun from(entity: DiscordGuildPreview) = with(entity) {
                        GuildPreviewData(
                                id.toLong(),
                                name,
                                icon,
                                splash,
                                discoverySplash,
                                emojis.map { EmojiData.from(guildId = id ,id = it.id!!, entity = it) },
                                features,
                                approximateMemberCount,
                                approximatePresenceCount,
                                description
                        )
                }
        }

}

package com.gitlab.kordlib.core.cache.data

import com.gitlab.kordlib.rest.json.response.DiscordIntegrationsAccount
import com.gitlab.kordlib.rest.json.response.IntegrationExpireBehavior
import com.gitlab.kordlib.rest.json.response.IntegrationResponse
import kotlinx.serialization.Serializable

@Serializable
data class IntegrationData(
        val id: Long,
        val name: String,
        val type: String,
        val enabled: Boolean,
        val syncing: Boolean,
        val roleId: Long,
        val guildId: Long,
        val enableEmoticons: Boolean = false,
        val expireBehavior: IntegrationExpireBehavior,
        val expireGracePeriod: Int,
        val userId: Long,
        //no clue what this is, we'll ignore it for now
        val account: DiscordIntegrationsAccount,
        val syncedAt: String
) {

    companion object {

        fun from(guildId: Long, response: IntegrationResponse) = with(response) {
            IntegrationData(
                    id = id.toLong(),
                    name = name,
                    type=  type,
                    guildId = guildId,
                    roleId = roleId.toLong(),
                    account = account,
                    enabled = enabled,
                    enableEmoticons = enableEmoticons ?: false,
                    expireBehavior = expireBehavior,
                    expireGracePeriod = expireGracePeriod,
                    syncedAt = syncedAt,
                    syncing = syncing,
                    userId = user.id.toLong()
            )

        }

    }

}

package com.gitlab.kordlib.core.cache.data

import com.gitlab.kordlib.gateway.DiscordCreatedInvite
import kotlinx.serialization.Serializable

@Serializable
data class InviteCreateData(
        /**
         * The channel the invite is for.
         */
        val channelId: Long,
        /**
         * The unique invite code.
         */
        val code: String,
        /**
         * The time at which the invite was created.
         */
        val createdAt: String,
        /**
         * The guild of the invite.
         */
        val guildId: Long,
        /**
         * The user that created the invite.
         */
        val inviterId: Long,
        /**
         * How long the invite is valid for (in seconds).
         */
        val maxAge: Int,
        /**
         * The maximum number of times the invite can be used.
         */
        val maxUses: Int,
        /**
         * Whether or not the invite is temporary (invited users will be kicked on disconnect unless they're assigned a role).
         */
        val temporary: Boolean,
        /**
         * How many times the invite has been used (always will be 0).
         */
        val uses: Int
) {

    companion object {
        fun from(entity: DiscordCreatedInvite): InviteCreateData = with(entity) {
            InviteCreateData(channelId.toLong(), code, createdAt, guildId.toLong(), inviter.id.toLong(), maxAge, maxUses, temporary, uses)
        }
    }

}

package com.gitlab.kordlib.core.cache.data

import com.gitlab.kordlib.cache.api.data.description
import com.gitlab.kordlib.common.entity.TargetUserType
import com.gitlab.kordlib.rest.json.response.InviteResponse
import kotlinx.serialization.Serializable

@Serializable
data class InviteData(
        val code: String,
        val guild: PartialGuildData?,
        val channelId: Long,
        val targetUserId: Long?,
        val inviterId: Long?,
        val approximatePresenceCount: Int?,
        val approximateMemberCount: Int?,
        val targetUserType: TargetUserType?
) {

    companion object {

        fun from(entity: InviteResponse) = with(entity) {
            InviteData(
                    code,
                    guild?.let { PartialGuildData.from(it) },
                    channel.id.toLong(),
                    targetUser?.id?.toLong(),
                    inviter?.id?.toLong(),
                    approximatePresenceCount,
                    approximateMemberCount,
                    targetUserType
            )
        }
    }
}

package com.gitlab.kordlib.core.cache.data

import com.gitlab.kordlib.gateway.DiscordDeletedInvite
import kotlinx.serialization.Serializable

@Serializable
data class InviteDeleteData(
        /**
         * The channel of the invite.
         */
        val channelId: Long,
        /**
         * The guild of the invite.
         */
        val guildId: Long,
        /**
         * The unique invite code.
         */
        val code: String
) {

    companion object {
        fun from(entity: DiscordDeletedInvite): InviteDeleteData = with(entity) {
            InviteDeleteData(channelId.toLong(), guildId.toLong(), code)
        }
    }
}

package com.gitlab.kordlib.core.cache.data

import com.gitlab.kordlib.cache.api.data.description
import com.gitlab.kordlib.common.entity.DiscordAddedGuildMember
import com.gitlab.kordlib.common.entity.DiscordGuildMember
import com.gitlab.kordlib.common.entity.DiscordPartialGuildMember
import com.gitlab.kordlib.common.entity.DiscordUpdatedGuildMember
import kotlinx.serialization.Serializable

private val MemberData.id get() = "$userId$guildId"

@Serializable
data class MemberData(
        val userId: Long,
        val guildId: Long,
        val nick: String? = null,
        val roles: List<String>,
        val joinedAt: String,
        val premiumSince: String?
) {
    operator fun plus(update: DiscordUpdatedGuildMember) =
            copy(nick = update.nick, roles = update.roles, premiumSince = update.premiumSince)

    companion object {
        val description = description(MemberData::id)

        fun from(userId: String, guildId: String, entity: DiscordGuildMember) =
                with(entity) { MemberData(userId.toLong(), guildId.toLong(), nick, roles, joinedAt, premiumSince) }

        fun from(userId: String, entity: DiscordAddedGuildMember) =
                with(entity) { MemberData(userId.toLong(), guildId.toLong(), nick, roles, joinedAt, premiumSince) }

        fun from(userId: String, guildId: String, entity: DiscordPartialGuildMember) =
                with(entity) { MemberData(userId.toLong(), guildId.toLong(), nick, roles, joinedAt, premiumSince) }

    }
}

fun DiscordGuildMember.toData(userId: String, guildId: String) = MemberData.from(userId, guildId, this)

package com.gitlab.kordlib.core.cache.data

import com.gitlab.kordlib.cache.api.data.description
import com.gitlab.kordlib.common.entity.*
import kotlinx.serialization.Serializable

internal val MessageData.authorId get() = author?.id

@Serializable
data class MessageData(
        val id: Long,
        val channelId: Long,
        @Deprecated("guildId's presence is inconsistent and will be removed in later versions")
        val guildId: Long? = null,
        val author: UserData,
        val content: String,
        val timestamp: String,
        val editedTimestamp: String? = null,
        val tts: Boolean,
        val mentionEveryone: Boolean,
        val mentions: List<Long>,
        val mentionRoles: List<Long>,
        val attachments: List<AttachmentData>,
        val embeds: List<EmbedData>,
        val reactions: List<ReactionData>? = null,
        val nonce: Long? = null,
        val pinned: Boolean,
        val webhookId: Long?,
        val type: MessageType,
        val activity: MessageActivity? = null,
        val application: MessageApplication? = null,
        val mentionedChannels: List<Long>? = null
) {

    fun plus(selfId: Snowflake, reaction: MessageReaction): MessageData {
        val isMe = selfId.value == reaction.userId

        val reactions = if (reactions.isNullOrEmpty()) {
            listOf(ReactionData.from(1, isMe, reaction.emoji))
        } else {
            val reactions = reactions.orEmpty()
            val data = reactions.firstOrNull { data ->
                if (reaction.emoji.id == null) data.emojiName == reaction.emoji.name
                else data.emojiId?.toString() == reaction.emoji.id && data.emojiName == reaction.emoji.name
            }

            when (data) {
                null -> reactions + ReactionData.from(1, isMe, reaction.emoji)
                else -> (reactions - data) + data.copy(count = data.count + 1, me = isMe)
            }
        }

        return copy(reactions = reactions)
    }

    operator fun plus(partialMessage: DiscordPartialMessage): MessageData {

        val editedTimestamp = partialMessage.editedTimestamp ?: editedTimestamp
        val content = partialMessage.content ?: content
        val mentions = partialMessage.mentions.orEmpty().map { it.id.toLong() }
        val mentionEveryone = partialMessage.mentionEveryone ?: mentionEveryone
        val embeds = partialMessage.embeds?.map { EmbedData.from(it) } ?: embeds
        val mentionRoles = partialMessage.mentionRoles?.map { it.toLong() } ?: mentionRoles
        val mentionedChannels = partialMessage.mentionedChannels?.map { it.id.toLong() } //can't figure out if list hasn't been updated or just isn't there, so we'll assume the former

        return MessageData(
                id,
                channelId,
                guildId,
                author,
                content,
                timestamp,
                editedTimestamp,
                tts,
                mentionEveryone,
                mentions,
                mentionRoles,
                attachments,
                embeds,
                reactions,
                nonce,
                pinned,
                webhookId,
                type,
                activity,
                application,
                mentionedChannels
        )
    }

    companion object {
        val description = description(MessageData::id)

        fun from(entity: DiscordMessage) = with(entity) {
            MessageData(
                    id.toLong(),
                    channelId.toLong(),
                    guildId?.toLong(),
                    UserData.from(author),
                    content,
                    timestamp,
                    editedTimestamp,
                    tts,
                    mentionEveryone,
                    mentions.map { it.id.toLong() },
                    mentionRoles.map { it.toLong() },
                    attachments.map { AttachmentData.from(it) },
                    embeds.map { EmbedData.from(it) },
                    reactions?.map { ReactionData.from(it) },
                    nonce?.toLong(),
                    pinned,
                    webhookId?.toLong(),
                    type,
                    activity,
                    application,
                    mentionedChannels?.map { it.id.toLong() }
            )
        }
    }
}

fun DiscordMessage.toData() = MessageData.from(this)

package com.gitlab.kordlib.core.cache.data

import com.gitlab.kordlib.cache.api.data.description
import com.gitlab.kordlib.common.entity.DiscordPartialGuild
import com.gitlab.kordlib.common.entity.Permissions
import kotlinx.serialization.Serializable

@Serializable
class PartialGuildData(
        val id: Long,
        val name: String,
        val icon: String? = null,
        val owner: Boolean? = null,
        val permissions: Permissions? = null
) {
    companion object {

        fun from(partialGuild: DiscordPartialGuild) = with(partialGuild) {
            PartialGuildData(id.toLong(), name, icon, owner, permissions)
        }
    }

}

package com.gitlab.kordlib.core.cache.data

import com.gitlab.kordlib.common.entity.Overwrite
import kotlinx.serialization.Serializable

@Serializable
data class PermissionOverwriteData(
        val id: Long,
        val type: String,
        val allowed: Int,
        val denied: Int
) {
    companion object {
        fun from(entity: Overwrite) = with(entity) {
            PermissionOverwriteData(id.toLong(), type, allow, deny)
        }
    }
}

package com.gitlab.kordlib.core.cache.data

import com.gitlab.kordlib.cache.api.data.description
import com.gitlab.kordlib.common.entity.DiscordPresenceUpdateData
import com.gitlab.kordlib.common.entity.Status
import kotlinx.serialization.Serializable

val PresenceData.id get() = "$userId$guildId"

@Serializable
data class PresenceData(
        val userId: Long,
        val roles: List<Long>? = null,
        val game: ActivityData?,
        val guildId: Long? = null,
        val status: Status,
        val activities: List<ActivityData>,
        val clientStatus: ClientStatusData
) {

    companion object {
        val description = description(PresenceData::id)

        fun from(guildId: String, entity: DiscordPresenceUpdateData) = with(entity) {
            PresenceData(
                    user.id.toLong(),
                    roles?.map { it.toLong() },
                    game?.let { ActivityData.from(it) },
                    guildId.toLong(),
                    status,
                    activities.map { ActivityData.from(it) },
                    ClientStatusData.from(clientStatus)
            )
        }
    }

}

package com.gitlab.kordlib.core.cache.data

import com.gitlab.kordlib.common.entity.DiscordPartialEmoji
import com.gitlab.kordlib.common.entity.Reaction
import kotlinx.serialization.Serializable

@Serializable
data class ReactionData(
        val count: Int,
        val me: Boolean,
        val emojiId: Long? = null,
        val emojiName: String? = null,
        val emojiAnimated: Boolean
) {
    companion object {
        fun from(entity: Reaction) = with(entity) {
            ReactionData(count, me, emoji.id?.toLong(), emoji.name, emoji.animated ?: false)
        }

        fun from(count: Int, me: Boolean, entity: DiscordPartialEmoji) = with(entity) {
            ReactionData(count, me, id?.toLong(), name, animated ?: false)
        }
    }

}

package com.gitlab.kordlib.core.cache.data

import com.gitlab.kordlib.gateway.DiscordRemovedEmoji
import com.gitlab.kordlib.gateway.DiscordRemovedReactionEmoji
import kotlinx.serialization.Serializable

@Serializable
data class RemovedReactionData(val id: Long?, val name: String) {
    companion object {
        fun from(entity: DiscordRemovedReactionEmoji): RemovedReactionData = with(entity) {
            RemovedReactionData(id?.toLong(), name)
        }
    }
}

@Serializable
data class ReactionRemoveEmojiData(
        /**
         * The id of the channel.
         */
        val channelId: Long,

        /**
         * The id of the guild.
         */
        val guildId: Long,

        /**
         * The id of the message.
         */
        val messageId: Long,

        /**
         * The emoji that was removed.
         */
        val emoji: RemovedReactionData
) {

    companion object {
        fun from(entity: DiscordRemovedEmoji): ReactionRemoveEmojiData = with(entity) {
            ReactionRemoveEmojiData(channelId.toLong(), guildId.toLong(), messageId.toLong(), RemovedReactionData.from(emoji))
        }
    }

}

package com.gitlab.kordlib.core.cache.data

import com.gitlab.kordlib.rest.json.response.VoiceRegion
import kotlinx.serialization.Serializable

@Serializable
data class RegionData(
        val id: Long,
        val guildId: Long?,
        val name: String,
        val vip: Boolean,
        val optimal: Boolean,
        val deprecated: Boolean,
        val custom: Boolean
) {
    companion object {
        fun from(guildId: String?, region: VoiceRegion) = with(region) {
            RegionData(id.toLong(), guildId?.toLong(), name, vip, optimal, deprecated, custom)
        }
    }
}

package com.gitlab.kordlib.core.cache.data

import com.gitlab.kordlib.cache.api.data.description
import com.gitlab.kordlib.common.entity.DiscordGuildRole
import com.gitlab.kordlib.common.entity.DiscordRole
import com.gitlab.kordlib.common.entity.Permissions
import kotlinx.serialization.Serializable

@Serializable
data class RoleData(
        val id: Long,
        val guildId: Long,
        val name: String,
        val color: Int,
        val hoisted: Boolean,
        val position: Int,
        val permissions: Permissions,
        val managed: Boolean,
        val mentionable: Boolean
) {
    companion object {
        val description = description(RoleData::id)
        fun from(guildId: String, entity: DiscordRole) = with(entity) { RoleData(id.toLong(), guildId.toLong(), name, color, hoist, position, permissions, managed, mentionable) }
        fun from(entity: DiscordGuildRole) = from(entity.guildId, entity.role)

    }
}

package com.gitlab.kordlib.core.cache.data

import com.gitlab.kordlib.common.entity.TeamMembershipState
import kotlinx.serialization.Serializable

/**
 * A serializable data representation of a Discord developer team.
 */
@Serializable
data class TeamData(
        /**
         * The unique ID of this team.
         */
        val id: Long,
        /**
         * The hash of this team's icon.
         */
        val icon: String?,
        /**
         * A collection of all members of this team.
         */
        val members: List<TeamMemberData>,
        /**
         * The ID of the user that owns the team.
         */
        val ownerUserId: Long
)

/**
 * A serializable data representation of a Discord developer team member.
 */
@Serializable
class TeamMemberData(
        /**
         * An enumeration representing the membership state of this user.
         */
        val membershipState: TeamMembershipState,
        /**
         * A collection of permissions granted to this member.
         * At the moment, this collection will only have one element: `*`, meaning the member has all permissions.
         * This is because right now there are no other permissions. Read mode [here](https://discord.com/developers/docs/topics/teams#data-models-team-members-object)
         */
        val permissions: List<String>,
        /**
         * The unique ID that this member belongs to.
         */
        val teamId: Long,
        /**
         * The ID of the user this member represents.
         */
        val userId: Long
)

package com.gitlab.kordlib.core.cache.data

import com.gitlab.kordlib.cache.api.data.description
import com.gitlab.kordlib.common.entity.DiscordUser
import com.gitlab.kordlib.common.entity.Premium
import com.gitlab.kordlib.common.entity.UserFlags
import kotlinx.serialization.Serializable

@Serializable
data class UserData(
        val id: Long,
        val username: String,
        val discriminator: String,
        val avatar: String? = null,
        val bot: Boolean? = null,
        val flags: UserFlags? = null,
        val premium: Premium? = null
) {
    companion object {

        val description
            get() = description(UserData::id) {
                link(UserData::id to MemberData::userId)
                link(UserData::id to WebhookData::userid)
                link(UserData::id to VoiceStateData::userId)
                link(UserData::id to PresenceData::userId)
            }

        fun from(entity: DiscordUser) = with(entity) {
            UserData(
                    id.toLong(),
                    username,
                    discriminator,
                    avatar,
                    bot,
                    flags,
                    premiumType
            )
        }

    }
}

fun DiscordUser.toData() = UserData.from(this)

package com.gitlab.kordlib.core.cache.data

import com.gitlab.kordlib.cache.api.data.description
import com.gitlab.kordlib.common.entity.DiscordVoiceState
import kotlinx.serialization.Serializable

val VoiceStateData.id get() = "$userId$guildId"

@Serializable
data class VoiceStateData(
        val guildId: Long?,
        val channelId: Long?,
        val userId: Long,
        val sessionId: String,
        val mute: Boolean,
        val deaf: Boolean,
        val selfMute: Boolean,
        val selfDeaf: Boolean,
        val selfStream: Boolean,
        val suppress: Boolean
) {

    companion object {
        val description = description(VoiceStateData::id)

        fun from(guildId: String?, entity: DiscordVoiceState) = with(entity) {
            VoiceStateData(
                    guildId?.toLong(),
                    channelId?.toLong(),
                    userId.toLong(),
                    sessionId,
                    mute,
                    deaf,
                    selfMute,
                    selfDeaf,
                    selfStream ?: false,
                    suppress
            )
        }
    }

}

package com.gitlab.kordlib.core.cache.data

import com.gitlab.kordlib.cache.api.data.description
import com.gitlab.kordlib.common.entity.DiscordWebhook
import com.gitlab.kordlib.common.entity.WebhookType
import kotlinx.serialization.Serializable

@Serializable
data class WebhookData(
        val id: Long,
        val type: WebhookType,
        val guildId: Long,
        val channelId: Long,
        val userid: Long,
        val name: String? = null,
        val avatar: String? = null,
        val token: String? = null
) {
    companion object {
        val description = description(WebhookData::id)

        fun from(entity: DiscordWebhook) = with(entity) { WebhookData(id.toLong(), type, guildId!!.toLong(), channelId.toLong(), user!!.id.toLong(), name, avatar, token) }
    }
}

package com.gitlab.kordlib.core.entity.channel

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.behavior.channel.CategoryBehavior
import com.gitlab.kordlib.core.cache.data.InviteData
import com.gitlab.kordlib.core.entity.Invite
import com.gitlab.kordlib.core.toSnowflakeOrNull
import com.gitlab.kordlib.rest.builder.channel.InviteCreateBuilder
import com.gitlab.kordlib.rest.request.RestRequestException
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

/**
 * An instance of a Discord channel associated to a [category].
 */
interface CategorizableChannel : GuildChannel {

    /**
     * The id of the [category] this channel belongs to, if any.
     */
    val categoryId: Snowflake?
        get() = data.parentId.toSnowflakeOrNull()

    /**
     * The category behavior this channel belongs to, if any.
     */
    val category: CategoryBehavior?
        get() = when (val categoryId = categoryId) {
            null -> null
            else -> CategoryBehavior(id = categoryId, guildId = guildId, kord = kord)
        }

    /**
     * Request to create an invite for this channel.
     *
     * @return the created [Invite].
     * @throws RestRequestException if something went wrong during the request.
     */
    suspend fun createInvite(builder: InviteCreateBuilder.() -> Unit): Invite {
        val response = kord.rest.channel.createInvite(id.value, builder)
        val data = InviteData.from(response)

        return Invite(data, kord)
    }

}

package com.gitlab.kordlib.core.entity.channel

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.channel.CategoryBehavior
import com.gitlab.kordlib.core.behavior.channel.ChannelBehavior
import com.gitlab.kordlib.core.behavior.channel.GuildChannelBehavior
import com.gitlab.kordlib.core.cache.data.ChannelData
import com.gitlab.kordlib.core.entity.Entity
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import java.util.*

/**
 * An instance of a Discord category associated to a [guild].
 */
class Category(
        override val data: ChannelData,
        override val kord: Kord,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : GuildChannel, CategoryBehavior {

    override val guildId: Snowflake
        get() = super.guildId

    override val guild get() = super<GuildChannel>.guild

    override suspend fun asChannel(): Category = this

    override fun compareTo(other: Entity): Int {
        return super<GuildChannel>.compareTo(other)
    }

    /**
     * Returns a new [Category] with the given [strategy].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>): Category =
            Category(data, kord, strategy.supply(kord))

    override fun hashCode(): Int = Objects.hash(id, guildId)

    override fun equals(other: Any?): Boolean = when (other) {
        is GuildChannelBehavior -> other.id == id && other.guildId == guildId
        is ChannelBehavior -> other.id == id
        else -> false
    }

}

package com.gitlab.kordlib.core.entity.channel

import com.gitlab.kordlib.common.entity.ChannelType
import com.gitlab.kordlib.common.entity.ChannelType.*
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.channel.ChannelBehavior
import com.gitlab.kordlib.core.cache.data.ChannelData
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy

/**
 * An instance of a [Discord Channel](https://discord.com/developers/docs/resources/channel)
 */
interface Channel : ChannelBehavior {
    val data: ChannelData

    override val id: Snowflake
        get() = Snowflake(data.id)

    /**
     * The type of this channel.
     */
    val type: ChannelType get() = data.type

    /**
     * Returns a new [Channel] with the given [strategy].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>): Channel =
            from(data, kord, strategy)

    companion object {

        /**
         * Creates a [Channel] of the type defined in the [ChannelData.type].
         * If the type is not any known type, then an anonymous channel will be created.
         */
        fun from(
                data: ChannelData,
                kord: Kord,
                strategy: EntitySupplyStrategy<*> = kord.resources.defaultStrategy
        ): Channel = when (data.type) {
            GuildText -> TextChannel(data, kord)
            DM, GroupDm -> DmChannel(data, kord)
            GuildVoice -> VoiceChannel(data, kord)
            GuildCategory -> Category(data, kord)
            GuildNews -> NewsChannel(data, kord)
            GuildStore -> StoreChannel(data, kord)
            else -> object : Channel {
                override val data: ChannelData = data
                override val kord: Kord = kord
                override val supplier: EntitySupplier = strategy.supply(kord)
            }
        }
    }
}

package com.gitlab.kordlib.core.entity.channel

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.UserBehavior
import com.gitlab.kordlib.core.behavior.channel.ChannelBehavior
import com.gitlab.kordlib.core.cache.data.ChannelData
import com.gitlab.kordlib.core.entity.User
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import java.util.*

/**
 * An instance of a Discord DM channel.
 */
data class DmChannel(
        override val data: ChannelData,
        override val kord: Kord,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : MessageChannel {
    /**
     * The ids of the recipients of the channel.
     */
    val recipientIds: Set<Snowflake>
        get() = data.recipients.orEmpty().asSequence()
                .map { Snowflake(it) }
                .toSet()

    /**
     * The behaviors of the recipients of the channel.
     */
    val recipientBehaviors: Set<UserBehavior> get() = recipientIds.map { UserBehavior(it, kord) }.toSet()

    /**
     * Requests to get the recipients of the channel.
     *
     * This request uses state [data] to resolve the entities belonging to the flow,
     * as such it can't guarantee an up to date representation if the [data] is outdated.
     *
     * The returned flow is lazily executed, any [RequestException] will be thrown on
     * [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
     */
    val recipients: Flow<User>
        get() = data.recipients.orEmpty().asFlow()
                .map { supplier.getUserOrNull(Snowflake(it)) }
                .filterNotNull()

    /**
     * returns a new [DmChannel] with the given [strategy].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>): DmChannel =
            DmChannel(data, kord, strategy.supply(kord))

    override fun hashCode(): Int = Objects.hash(id)

    override fun equals(other: Any?): Boolean = when(other) {
        is ChannelBehavior -> other.id == id
        else -> false
    }
}

package com.gitlab.kordlib.core.entity.channel

import com.gitlab.kordlib.common.entity.Permission
import com.gitlab.kordlib.common.entity.Permissions
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.behavior.channel.GuildChannelBehavior
import com.gitlab.kordlib.core.cache.data.PermissionOverwriteData
import com.gitlab.kordlib.core.entity.PermissionOverwrite
import com.gitlab.kordlib.core.entity.PermissionOverwriteEntity
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy

/**
 * An instance of a Discord channel associated to a [guild].
 */
interface GuildChannel : Channel, GuildChannelBehavior {

    override val guildId: Snowflake
        get() = Snowflake(data.guildId!!)

    /**
     * The name of this channel.
     */
    val name get() = data.name!!

    /**
     * The raw position of this channel in the guild as displayed by Discord.
     */
    val rawPosition get() = data.position!!

    /**
     * The permission overwrites for this channel.
     */
    val permissionOverwrites: Set<PermissionOverwriteEntity>
        get() = data.permissionOverwrites.asSequence()
                .map { PermissionOverwriteData(it.id, it.type, it.allowed, it.denied) }
                .map { PermissionOverwriteEntity(guildId, id, it, kord) }
                .toSet()

    /**
     * Calculates the effective permissions of the [memberId] in this channel, applying the overwrite for the member
     * and their roles on top of the base permissions.
     *
     * @throws [RequestException] if something went wrong during the request.
     * @throws IllegalArgumentException if the [memberId] is not part of this guild.
     */
    suspend fun getEffectivePermissions(memberId: Snowflake): Permissions {
        val member = supplier.getMemberOrNull(guildId, memberId)
        require(member != null) {
            "member ${memberId.value} is not in guild ${guildId.value}"
        }

        val base = member.getPermissions()

        if (Permission.Administrator in base) return Permissions { +Permission.All }

        val everyoneOverwrite = getPermissionOverwritesForRole(guildId)
        val roleOverwrites = member.roleIds.mapNotNull { getPermissionOverwritesForRole(it) }
        val memberOverwrite = getPermissionOverwritesForMember(memberId)

        return Permissions {
            +base
            everyoneOverwrite?.let {
                +it.allowed
                -it.denied
            }
            roleOverwrites.map {
                +it.allowed
                -it.denied
            }
            memberOverwrite?.let {
                +it.allowed
                -it.denied
            }
        }

    }

    /**
     * Gets the permission overwrite for the [memberId] in this channel, if present.
     */
    fun getPermissionOverwritesForMember(memberId: Snowflake): PermissionOverwriteEntity? =
            getPermissionOverwritesForType(memberId, PermissionOverwrite.Type.Member)

    /**
     * Gets the permission overwrite for the [roleId] in this channel, if present.
     */
    fun getPermissionOverwritesForRole(roleId: Snowflake): PermissionOverwriteEntity? =
            getPermissionOverwritesForType(roleId, PermissionOverwrite.Type.Role)

    private fun getPermissionOverwritesForType(id: Snowflake, type: PermissionOverwrite.Type): PermissionOverwriteEntity? =
            permissionOverwrites.firstOrNull { it.target == id && it.type == type }

    override fun withStrategy(strategy: EntitySupplyStrategy<*>): GuildChannel

}

package com.gitlab.kordlib.core.entity.channel

import com.gitlab.kordlib.core.behavior.channel.GuildMessageChannelBehavior
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy

/**
 * An instance of a Discord message channel associated to a [guild].
 */
interface GuildMessageChannel : CategorizableChannel, MessageChannel, GuildMessageChannelBehavior {

    /**
     * The channel topic, if present.
     */
    val topic: String? get() = data.topic

    /**
     * Returns a new [GuildMessageChannel] with the given [strategy].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>): GuildMessageChannel

}

package com.gitlab.kordlib.core.entity.channel

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.behavior.MessageBehavior
import com.gitlab.kordlib.core.behavior.channel.MessageChannelBehavior
import com.gitlab.kordlib.core.entity.Message
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.core.toInstant
import com.gitlab.kordlib.core.toSnowflakeOrNull
import java.time.Instant

/**
 * An instance of a Discord channel that can use messages.
 */
interface MessageChannel : Channel, MessageChannelBehavior {

    /**
     * The id of the last message sent to this channel, if present.
     */
    val lastMessageId: Snowflake? get() = data.lastMessageId.toSnowflakeOrNull()

    /**
     * The behavior of the last message sent to this channel, if present.
     */
    val lastMessage: MessageBehavior? get() = lastMessageId?.let { MessageBehavior(id, it, kord) }

    /**
     * The timestamp of the last pin
     */
    val lastPintTimeStamp: Instant?
        get() = data.lastPinTimestamp?.toInstant()

    /**
     * Requests to get the last message sent to this channel,
     * return null if no [lastMessageId] is present or if the message itself isn't present.
     *
     * @throws [RequestException] if something went wrong during the request.
     */
    suspend fun getLastMessage(): Message? {
        val messageId = lastMessageId ?: return null

        return supplier.getMessageOrNull(id, messageId)
    }

    /**
     * Returns a new [MessageChannel] with the given [strategy].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>): MessageChannel

}

package com.gitlab.kordlib.core.entity.channel

import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.channel.ChannelBehavior
import com.gitlab.kordlib.core.behavior.channel.GuildChannelBehavior
import com.gitlab.kordlib.core.behavior.channel.NewsChannelBehavior
import com.gitlab.kordlib.core.cache.data.ChannelData
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import java.util.*

/**
 * An instance of a Discord News Channel associated to a guild.
 */
data class NewsChannel(
        override val data: ChannelData,
        override val kord: Kord,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : CategorizableChannel, GuildMessageChannel, NewsChannelBehavior {

    override suspend fun asChannel(): NewsChannel = this

    override fun hashCode(): Int = Objects.hash(id, guildId)

    override fun equals(other: Any?): Boolean = when (other) {
        is GuildChannelBehavior -> other.id == id && other.guildId == guildId
        is ChannelBehavior -> other.id == id
        else -> false
    }

    /**
     * Returns a new [NewsChannel] with the given [strategy].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>): NewsChannel =
            NewsChannel(data, kord, strategy.supply(kord))
}

package com.gitlab.kordlib.core.entity.channel

import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.channel.ChannelBehavior
import com.gitlab.kordlib.core.behavior.channel.GuildChannelBehavior
import com.gitlab.kordlib.core.behavior.channel.StoreChannelBehavior
import com.gitlab.kordlib.core.cache.data.ChannelData
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import java.util.*

/**
 * An instance of a Discord Store Channel associated to a guild.
 */
data class StoreChannel(
        override val data: ChannelData,
        override val kord: Kord,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : CategorizableChannel, GuildChannel, StoreChannelBehavior {

    override suspend fun asChannel(): StoreChannel = this

    /**
     * Returns a new [StoreChannel] with the given [strategy].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>): StoreChannel =
            StoreChannel(data, kord, strategy.supply(kord))

    override fun hashCode(): Int = Objects.hash(id, guildId)

    override fun equals(other: Any?): Boolean = when(other) {
        is GuildChannelBehavior -> other.id == id && other.guildId == guildId
        is ChannelBehavior -> other.id == id
        else -> false
    }
}

package com.gitlab.kordlib.core.entity.channel

import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.channel.ChannelBehavior
import com.gitlab.kordlib.core.behavior.channel.GuildChannelBehavior
import com.gitlab.kordlib.core.behavior.channel.TextChannelBehavior
import com.gitlab.kordlib.core.cache.data.ChannelData
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import java.util.*

/**
 * An instance of a Discord Text Channel associated to a guild.
 */
class TextChannel(
        override val data: ChannelData,
        override val kord: Kord,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : GuildMessageChannel, TextChannelBehavior {

    /**
     * Whether the channel is nsfw.
     */
    val isNsfw: Boolean get() = data.nsfw!!

    /**
     * The amount of seconds a user has to wait before sending another message.
     */
    val userRateLimit: Int get() = data.rateLimitPerUser!!

    /**
     * returns a new [TextChannel] with the given [strategy].
     *
     * @param strategy the strategy to use for the new instance. By default [EntitySupplyStrategy.CacheWithRestFallback].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>): TextChannel =
            TextChannel(data, kord, strategy.supply(kord))

    override fun hashCode(): Int = Objects.hash(id, guildId)

    override fun equals(other: Any?): Boolean = when(other) {
        is GuildChannelBehavior -> other.id == id && other.guildId == guildId
        is ChannelBehavior -> other.id == id
        else -> false
    }

}

package com.gitlab.kordlib.core.entity.channel

import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.channel.ChannelBehavior
import com.gitlab.kordlib.core.behavior.channel.GuildChannelBehavior
import com.gitlab.kordlib.core.behavior.channel.VoiceChannelBehavior
import com.gitlab.kordlib.core.cache.data.ChannelData
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import java.util.*

/**
 * An instance of a Discord Voice Channel associated to a guild.
 */
class VoiceChannel(
        override val data: ChannelData,
        override val kord: Kord,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : CategorizableChannel, VoiceChannelBehavior {

    /**
     * The bitrate (in bits) of this channel.
     */
    val bitrate: Int get() = data.bitrate!!

    /**
     * The user limit of the voice channel.
     */
    val userLimit: Int get() = data.userLimit!!

    /**
     * returns a new [VoiceChannel] with the given [strategy].
     *
     * @param strategy the strategy to use for the new instance. By default [EntitySupplyStrategy.CacheWithRestFallback].
     */
    override fun withStrategy(strategy: EntitySupplyStrategy<*>): VoiceChannel = VoiceChannel(data, kord, strategy.supply(kord))

    override suspend fun asChannel(): VoiceChannel = this

    override fun hashCode(): Int = Objects.hash(id, guildId)

    override fun equals(other: Any?): Boolean = when(other) {
        is GuildChannelBehavior -> other.id == id && other.guildId == guildId
        is ChannelBehavior -> other.id == id
        else -> false
    }
}

package com.gitlab.kordlib.core.event.channel

import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.entity.channel.*
import com.gitlab.kordlib.core.event.Event

interface ChannelCreateEvent : Event {
    val channel: Channel
    override val kord: Kord
        get() = channel.kord
}

class CategoryCreateEvent (override val channel: Category, override val shard: Int) : ChannelCreateEvent
class DMChannelCreateEvent (override val channel: DmChannel, override val shard: Int) : ChannelCreateEvent
class NewsChannelCreateEvent (override val channel: NewsChannel, override val shard: Int) : ChannelCreateEvent
class StoreChannelCreateEvent (override val channel: StoreChannel, override val shard: Int) : ChannelCreateEvent
class TextChannelCreateEvent (override val channel: TextChannel, override val shard: Int) : ChannelCreateEvent
class VoiceChannelCreateEvent (override val channel: VoiceChannel, override val shard: Int) : ChannelCreateEvent

package com.gitlab.kordlib.core.event.channel

import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.entity.channel.*
import com.gitlab.kordlib.core.event.Event

interface ChannelDeleteEvent : Event {
    val channel: Channel
    override val kord: Kord
        get() = channel.kord
}

class CategoryDeleteEvent (override val channel: Category, override val shard: Int) : ChannelDeleteEvent
class DMChannelDeleteEvent (override val channel: DmChannel, override val shard: Int) : ChannelDeleteEvent
class NewsChannelDeleteEvent (override val channel: NewsChannel, override val shard: Int) : ChannelDeleteEvent
class StoreChannelDeleteEvent (override val channel: StoreChannel, override val shard: Int) : ChannelDeleteEvent
class TextChannelDeleteEvent (override val channel: TextChannel, override val shard: Int) : ChannelDeleteEvent
class VoiceChannelDeleteEvent (override val channel: VoiceChannel, override val shard: Int) : ChannelDeleteEvent

package com.gitlab.kordlib.core.event.channel

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.channel.MessageChannelBehavior
import com.gitlab.kordlib.core.entity.Strategizable
import com.gitlab.kordlib.core.entity.channel.MessageChannel
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.core.supplier.getChannelOf
import com.gitlab.kordlib.core.supplier.getChannelOfOrNull
import java.time.Instant

class ChannelPinsUpdateEvent(
        val channelId: Snowflake,
        val lastPinTimestamp: Instant?,
        override val kord: Kord,
        override val shard: Int,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : Event, Strategizable {

    val channel: MessageChannelBehavior get() = MessageChannelBehavior(channelId, kord)

    suspend fun getChannel(): MessageChannel = supplier.getChannelOf(channelId)

    suspend fun getChannelOrNull(): MessageChannel? = supplier.getChannelOfOrNull(channelId)

    override fun withStrategy(strategy: EntitySupplyStrategy<*>): ChannelPinsUpdateEvent =
            ChannelPinsUpdateEvent(channelId, lastPinTimestamp, kord, shard, strategy.supply(kord))
}

package com.gitlab.kordlib.core.event.channel

import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.entity.channel.*
import com.gitlab.kordlib.core.event.Event

interface ChannelUpdateEvent : Event {
    val channel: Channel
    override val kord: Kord
        get() = channel.kord
}

class CategoryUpdateEvent (override val channel: Category, override val shard: Int) : ChannelUpdateEvent
class DMChannelUpdateEvent (override val channel: DmChannel, override val shard: Int) : ChannelUpdateEvent
class NewsChannelUpdateEvent (override val channel: NewsChannel, override val shard: Int) : ChannelUpdateEvent
class StoreChannelUpdateEvent (override val channel: StoreChannel, override val shard: Int) : ChannelUpdateEvent
class TextChannelUpdateEvent (override val channel: TextChannel, override val shard: Int) : ChannelUpdateEvent
class VoiceChannelUpdateEvent (override val channel: VoiceChannel, override val shard: Int) : ChannelUpdateEvent

package com.gitlab.kordlib.core.event.channel

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.GuildBehavior
import com.gitlab.kordlib.core.behavior.UserBehavior
import com.gitlab.kordlib.core.behavior.channel.MessageChannelBehavior
import com.gitlab.kordlib.core.entity.Guild
import com.gitlab.kordlib.core.entity.Strategizable
import com.gitlab.kordlib.core.entity.User
import com.gitlab.kordlib.core.entity.channel.MessageChannel
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.exception.EntityNotFoundException
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.core.supplier.getChannelOf
import com.gitlab.kordlib.core.supplier.getChannelOfOrNull
import java.time.Instant

class TypingStartEvent(
        val channelId: Snowflake,
        val userId: Snowflake,
        val guildId: Snowflake?,
        val started: Instant,
        override val kord: Kord,
        override val shard: Int,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : Event, Strategizable {

    val channel: MessageChannelBehavior get() = MessageChannelBehavior(channelId, kord)

    val guild: GuildBehavior? get() = guildId?.let { GuildBehavior(it, kord) }

    val user: UserBehavior get() = UserBehavior(userId, kord)

    /**
     * Requests to get the channel triggering the event.
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the [MessageChannel] wasn't present.
     */
    suspend fun getChannel(): MessageChannel = supplier.getChannelOf(channelId)

    /**
     * Requests to get the channel triggering the event,
     * returns null if the [MessageChannel] isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getChannelOrNull(): MessageChannel? = supplier.getChannelOfOrNull(channelId)

    /**
     * Requests to get the user triggering the event.
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the [User] wasn't present.
     */
    suspend fun getUser(): User = supplier.getUser(userId)

    /**
     * Requests to get the user triggering the event,
     * returns null if the [User] isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getUserOrNull(): User? = supplier.getUserOrNull(userId)

    /**
     * Requests to get the guild this event was triggered in,
     * returns null if the [Guild] isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getGuild(): Guild? = guildId?.let { supplier.getGuildOrNull(it) }

    override fun withStrategy(strategy: EntitySupplyStrategy<*>): Strategizable =
            TypingStartEvent(
                    channelId,
                    userId,
                    guildId,
                    started,
                    kord,
                    shard,
                    supplier
            )

}

package com.gitlab.kordlib.core.event.gateway

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.entity.Guild
import com.gitlab.kordlib.core.entity.Strategizable
import com.gitlab.kordlib.core.entity.User
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.gateway.Close
import com.gitlab.kordlib.gateway.Command
import com.gitlab.kordlib.gateway.Gateway
import com.gitlab.kordlib.gateway.GatewayCloseCode
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter

sealed class GatewayEvent : Event

class ConnectEvent (override val kord: Kord, override val shard: Int) : GatewayEvent()

sealed class DisconnectEvent : GatewayEvent() {

    /**
     * A Gateway was detached, all resources tied to that gateway should be freed.
     */
    class DetachEvent(override val kord: Kord, override val shard: Int): DisconnectEvent()

    /**
     * The user closed the Gateway connection.
     */
    class UserCloseEvent(override val kord: Kord, override val shard: Int): DisconnectEvent()

    /**
     * The connection was closed because of a timeout, probably due to a loss of internet connection.
     */
    class TimeoutEvent(override val kord: Kord, override val shard: Int): DisconnectEvent()

    /**
     * Discord closed the connection with a [closeCode].
     *
     * @param recoverable true if the gateway will automatically try to reconnect.
     */
    class DiscordCloseEvent(override val kord: Kord, override val shard: Int, val closeCode: GatewayCloseCode, val recoverable: Boolean): DisconnectEvent()

    /**
     *  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 [Gateway.start], otherwise all resources linked to the Gateway should free and the Gateway [detached][Gateway.detach].
     */
    class RetryLimitReachedEvent(override val kord: Kord, override val shard: Int): DisconnectEvent()

    /**
     * Discord requested a reconnect, the gateway will close and attempt to resume the session.
     */
    class ReconnectingEvent(override val kord: Kord, override val shard: Int) : DisconnectEvent()

    /**
     * The gateway closed and will attempt to start a new session.
     */
    class SessionReset(override val kord: Kord, override val shard: Int) : DisconnectEvent()

    /**
     * Discord is no longer responding to the gateway commands, the connection will be closed and an attempt to resume the session will be made.
     * Any [commands][Command] send recently might not complete, and won't be automatically requeued.
     */
    class ZombieConnectionEvent(override val kord: Kord, override val shard: Int) : DisconnectEvent()

}

class ReadyEvent (
        val gatewayVersion: Int,
        val guildIds: Set<Snowflake>,
        val self: User,
        val sessionId: String,
        override val kord: Kord,
        override val shard: Int,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : GatewayEvent(), Strategizable {

    suspend fun getGuilds(): Flow<Guild> = supplier.guilds.filter { it.id in guildIds }

    override fun withStrategy(strategy: EntitySupplyStrategy<*>): ReadyEvent =
            ReadyEvent(gatewayVersion, guildIds, self, sessionId, kord, shard, strategy.supply(kord))
}

class ResumedEvent(override val kord: Kord, override val shard: Int) : GatewayEvent()

package com.gitlab.kordlib.core.event.guild

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.GuildBehavior
import com.gitlab.kordlib.core.entity.Ban
import com.gitlab.kordlib.core.entity.Guild
import com.gitlab.kordlib.core.entity.Strategizable
import com.gitlab.kordlib.core.entity.User
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.exception.EntityNotFoundException
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy

class BanAddEvent(
        val user: User,
        val guildId: Snowflake,
        override val shard: Int,
        override val supplier: EntitySupplier = user.kord.defaultSupplier
) : Event, Strategizable {

    override val kord: Kord get() = user.kord

    val guild: GuildBehavior get() = GuildBehavior(guildId, kord)

    /**
     * Requests to get the [Guild] this ban happened in.
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the guild wasn't present.
     */
    suspend fun getGuild(): Guild = supplier.getGuild(guildId)

    /**
     * Requests to get the [Guild] this ban happened in,
     * returns null if the guild isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getGuildOrNull(): Guild? = supplier.getGuildOrNull(guildId)

    /**
     * Requests to get the [Ban] entity this event represents.
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the ban wasn't present.
     */
    suspend fun getBan(): Ban = supplier.getGuildBan(guildId, user.id)

    /**
     * Requests to get the [Ban] entity this event represents.
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the ban wasn't present.
     */
    suspend fun getBanOrNull(): Ban? = supplier.getGuildBanOrNull(guildId, user.id)

    override fun withStrategy(strategy: EntitySupplyStrategy<*>): BanAddEvent =
            BanAddEvent(user, guildId, shard, strategy.supply(kord))
}

package com.gitlab.kordlib.core.event.guild

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.GuildBehavior
import com.gitlab.kordlib.core.entity.Guild
import com.gitlab.kordlib.core.entity.Strategizable
import com.gitlab.kordlib.core.entity.User
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy

class BanRemoveEvent(
        val user: User,
        val guildId: Snowflake,
        override val shard: Int,
        override val supplier: EntitySupplier = user.kord.defaultSupplier
) : Event, Strategizable {

    override val kord: Kord get() = user.kord

    val guild: GuildBehavior get() = GuildBehavior(guildId, kord)

    suspend fun getGuild(): Guild = supplier.getGuild(guildId)

    suspend fun getGuildOrNull(): Guild? = supplier.getGuildOrNull(guildId)

    override fun withStrategy(strategy: EntitySupplyStrategy<*>): BanRemoveEvent =
            BanRemoveEvent(user, guildId, shard, strategy.supply(kord))
}

package com.gitlab.kordlib.core.event.guild

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.GuildBehavior
import com.gitlab.kordlib.core.entity.Guild
import com.gitlab.kordlib.core.entity.GuildEmoji
import com.gitlab.kordlib.core.entity.Strategizable
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy

class EmojisUpdateEvent(
        val guildId: Snowflake,
        val emojis: Set<GuildEmoji>,
        override val kord: Kord,
        override val shard: Int,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : Event, Strategizable {

    val guild: GuildBehavior get() = GuildBehavior(guildId, kord)

    suspend fun getGuild(): Guild = supplier.getGuild(guildId)

    suspend fun getGuildOrNull(): Guild? = supplier.getGuildOrNull(guildId)

    override fun withStrategy(strategy: EntitySupplyStrategy<*>): EmojisUpdateEvent =
            EmojisUpdateEvent(guildId, emojis, kord, shard, strategy.supply(kord))
}

package com.gitlab.kordlib.core.event.guild

import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.entity.Guild
import com.gitlab.kordlib.core.event.Event

class GuildCreateEvent (val guild: Guild, override val shard: Int) : Event {
    override val kord: Kord get() = guild.kord
}

package com.gitlab.kordlib.core.event.guild

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.entity.Guild
import com.gitlab.kordlib.core.event.Event

class GuildDeleteEvent(
        val guildId: Snowflake,
        val unavailable: Boolean,
        val guild: Guild?,
        override val kord: Kord,
        override val shard: Int
) : Event

package com.gitlab.kordlib.core.event.guild

import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.entity.Guild
import com.gitlab.kordlib.core.event.Event

class GuildUpdateEvent (val guild: Guild, override val shard: Int) : Event {
    override val kord: Kord get() = guild.kord
}

package com.gitlab.kordlib.core.event.guild

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.GuildBehavior
import com.gitlab.kordlib.core.entity.Guild
import com.gitlab.kordlib.core.entity.Strategizable
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy

class IntegrationsUpdateEvent(
        val guildId: Snowflake,
        override val kord: Kord,
        override val shard: Int,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : Event, Strategizable {

    val guild: GuildBehavior get() = GuildBehavior(guildId, kord)

    suspend fun getGuild(): Guild = supplier.getGuild(guildId)

    suspend fun getGuildOrNull(): Guild? = supplier.getGuildOrNull(guildId)

    override fun withStrategy(strategy: EntitySupplyStrategy<*>): IntegrationsUpdateEvent =
            IntegrationsUpdateEvent(guildId, kord, shard, strategy.supply(kord))
}

package com.gitlab.kordlib.core.event.guild

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.GuildBehavior
import com.gitlab.kordlib.core.behavior.MemberBehavior
import com.gitlab.kordlib.core.behavior.UserBehavior
import com.gitlab.kordlib.core.behavior.channel.GuildChannelBehavior
import com.gitlab.kordlib.core.cache.data.InviteCreateData
import com.gitlab.kordlib.core.entity.Guild
import com.gitlab.kordlib.core.entity.Member
import com.gitlab.kordlib.core.entity.Strategizable
import com.gitlab.kordlib.core.entity.User
import com.gitlab.kordlib.core.entity.channel.GuildChannel
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.exception.EntityNotFoundException
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.core.supplier.getChannelOf
import com.gitlab.kordlib.core.supplier.getChannelOfOrNull
import com.gitlab.kordlib.core.toInstant
import java.time.Instant
import kotlin.time.Duration
import kotlin.time.seconds

/**
 * Sent when a new invite to a channel is created.
 */
class InviteCreateEvent(
        val data: InviteCreateData,
        override val kord: Kord,
        override val shard: Int,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : Event, Strategizable {

    /**
     * The [GuildChannel] the invite is for.
     */
    val channelId: Snowflake get() = Snowflake(data.channelId)

    /**
     * The behavior of the [GuildChannel] the invite is for.
     */
    val channel: GuildChannelBehavior get() = GuildChannelBehavior(guildId = guildId, id = channelId, kord = kord)

    /**
     * The unique invite code.
     */
    val code: String get() = data.code

    /**
     * The time at which the invite was created.
     */
    val createdAt: Instant get() = data.createdAt.toInstant()

    /**
     * The [Guild] of the invite.
     */
    val guildId: Snowflake get() = Snowflake(data.guildId)

    /**
     * The behavior of the [Guild] of the invite.
     */
    val guild: GuildBehavior get() = GuildBehavior(id = guildId, kord = kord)

    /**
     * The [User] that created the invite.
     */
    val inviterId: Snowflake get() = Snowflake(data.inviterId)

    /**
     * The behavior of the [User] that created the invite.
     */
    val inviter: UserBehavior get() = UserBehavior(id = inviterId, kord = kord)

    /**
     * The behavior of the [Member] that created the invite.
     */
    val inviterMember: MemberBehavior get() = MemberBehavior(guildId = guildId, id = inviterId, kord = kord)

    /**
     * How long the invite is valid for (in seconds).
     */
    val maxAge: Duration get() = data.maxAge.seconds

    /**
     * The maximum number of times the invite can be used.
     */
    val maxUses: Int get() = data.maxUses

    /**
     * Whether or not the invite is temporary (invited users will be kicked on disconnect unless they're assigned a role).
     */
    val isTemporary: Boolean get() = data.temporary

    /**
     * How many times the invite has been used (always will be 0).
     */
    val uses: Int get() = data.uses

    /**
     * Requests to get the [GuildChannel] this invite is for.
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the  wasn't present.
     */
    suspend fun getChannel(): GuildChannel = supplier.getChannelOf(channelId)

    /**
     * Requests to get the [Guild] of the invite,
     * returns null if the guild isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getChannelOrNUll(): GuildChannel? = supplier.getChannelOfOrNull(channelId)

    /**
     * Requests to get the [Guild] of the invite.
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the  wasn't present.
     */
    suspend fun getGuild(): Guild = supplier.getGuild(guildId)

    /**
     * Requests to get the [Guild] of the invite,
     * returns null if the guild isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getGuildOrNull(): Guild? = supplier.getGuildOrNull(guildId)

    /**
     * Requests to get the [User] that created the invite.
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the  wasn't present.
     */
    suspend fun getInviter(): User = supplier.getUser(inviterId)

    /**
     * Requests to get the [User] that created the invite,
     * returns null if the user isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getInviterOrNull(): User? = supplier.getUserOrNull(inviterId)

    /**
     * Requests to get the [User] that created the invite as a [Member] of the [Guild][getGuild].
     *
     * @throws [RequestException] if anything went wrong during the request.
     * @throws [EntityNotFoundException] if the  wasn't present.
     */
    suspend fun getInviterAsMember(): Member = supplier.getMember(guildId = guildId, userId = inviterId)

    /**
     * Requests to get the [User] that created the invite as a [Member] of the [Guild][getGuild],
     * returns null if the user isn't present.
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getInviterAsMemberOrNull(): Member? = supplier.getMemberOrNull(guildId = guildId, userId = inviterId)

    override fun withStrategy(strategy: EntitySupplyStrategy<*>): InviteCreateEvent =
            InviteCreateEvent(data, kord, shard, supplier)
}

package com.gitlab.kordlib.core.event.guild

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.GuildBehavior
import com.gitlab.kordlib.core.behavior.channel.GuildChannelBehavior
import com.gitlab.kordlib.core.cache.data.InviteDeleteData
import com.gitlab.kordlib.core.entity.Guild
import com.gitlab.kordlib.core.entity.Strategizable
import com.gitlab.kordlib.core.entity.channel.GuildChannel
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.core.supplier.getChannelOf
import com.gitlab.kordlib.core.supplier.getChannelOfOrNull

/**
 * Sent when an invite is deleted.
 */
class InviteDeleteEvent(
        val data: InviteDeleteData,
        override val kord: Kord,
        override val shard: Int,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : Event, Strategizable {

    /**
     * The [GuildChannel] of the invite.
     */
    val channelId: Snowflake get() = Snowflake(data.channelId)

    /**
     * The behavior of the [GuildChannel] of the invite.
     */
    val channel: GuildChannelBehavior get() = GuildChannelBehavior(guildId = guildId, id = channelId, kord = kord)

    /**
     * The [Guild] of the invite.
     */
    val guildId: Snowflake get() = Snowflake(data.guildId)

    /**
     * The behavior of the [Guild] of the invite.
     */
    val guild get() : GuildBehavior = GuildBehavior(id = guildId, kord = kord)

    /**
     * The unique invite code.
     */
    val code: String get() = data.code

    /**
     * Requests to get the [GuildChannel] of the invite.
     */
    suspend fun getChannel(): GuildChannel = supplier.getChannelOf(channelId)

    suspend fun getChannelOrNull(): GuildChannel? = supplier.getChannelOfOrNull(channelId)

    /**
     * Requests to get the [Guild] of the invite.
     */
    suspend fun getGuild(): Guild = supplier.getGuild(guildId)

    suspend fun getGuildOrNull(): Guild? = supplier.getGuildOrNull(guildId)

    override fun withStrategy(strategy: EntitySupplyStrategy<*>): InviteDeleteEvent =
            InviteDeleteEvent(data, kord, shard, strategy.supply(kord))

}

package com.gitlab.kordlib.core.event.guild

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.GuildBehavior
import com.gitlab.kordlib.core.entity.Guild
import com.gitlab.kordlib.core.entity.Member
import com.gitlab.kordlib.core.entity.Strategizable
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy

class MemberChunksEvent(
        val guildId: Snowflake,
        val members: Set<Member>,
        override val kord: Kord,
        override val shard: Int,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : Event, Strategizable {

    val guild: GuildBehavior get() = GuildBehavior(guildId, kord)

    suspend fun getGuild(): Guild = supplier.getGuild(guildId)

    suspend fun getGuildOrNull(): Guild? = supplier.getGuildOrNull(guildId)

    override fun withStrategy(strategy: EntitySupplyStrategy<*>): MemberChunksEvent =
            MemberChunksEvent(guildId, members, kord, shard, strategy.supply(kord))
}

package com.gitlab.kordlib.core.event.guild

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.GuildBehavior
import com.gitlab.kordlib.core.entity.Guild
import com.gitlab.kordlib.core.entity.Member
import com.gitlab.kordlib.core.entity.Strategizable
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy

class MemberJoinEvent(
        val member: Member,
        override val shard: Int,
        override val supplier: EntitySupplier = member.kord.defaultSupplier
) : Event, Strategizable {

    override val kord: Kord get() = member.kord

    val guildId: Snowflake get() = member.guildId

    val guild: GuildBehavior get() = member.guild

    suspend fun getGuild(): Guild = supplier.getGuild(guildId)

    suspend fun getGuildOrNull(): Guild? = supplier.getGuildOrNull(guildId)

    override fun withStrategy(strategy: EntitySupplyStrategy<*>): MemberJoinEvent =
            MemberJoinEvent(member, shard, strategy.supply(kord))

}

package com.gitlab.kordlib.core.event.guild

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.GuildBehavior
import com.gitlab.kordlib.core.entity.Guild
import com.gitlab.kordlib.core.entity.User
import com.gitlab.kordlib.core.event.Event

class MemberLeaveEvent (val user: User, val guildId: Snowflake, override val shard: Int) : Event {

    override val kord: Kord get() = user.kord

    val guild: GuildBehavior get() = GuildBehavior(guildId, kord)

    suspend fun getGuild(): Guild = guild.asGuild()

    suspend fun getGuildOrNull(): Guild? = guild.asGuildOrNull()

}

package com.gitlab.kordlib.core.event.guild

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.GuildBehavior
import com.gitlab.kordlib.core.behavior.MemberBehavior
import com.gitlab.kordlib.core.entity.Guild
import com.gitlab.kordlib.core.entity.Member
import com.gitlab.kordlib.core.entity.Role
import com.gitlab.kordlib.core.entity.Strategizable
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import java.time.Instant

class MemberUpdateEvent(
        val old: Member?,
        val guildId: Snowflake,
        val memberId: Snowflake,
        val currentRoleIds: Set<Snowflake>,
        val currentNickName: String,
        val premiumSince: Instant?,
        override val kord: Kord,
        override val shard: Int,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : Event, Strategizable {

    val member: MemberBehavior get() = MemberBehavior(guildId, memberId, kord)

    val guild: GuildBehavior get() = GuildBehavior(guildId, kord)

    val currentRoles: Flow<Role> get() = supplier
            .getGuildRoles(guildId)
            .filter { it.id in currentRoleIds }

    suspend fun getMember(): Member = supplier.getMember(guildId = guildId, userId = memberId)

    suspend fun getMemberOrNull(): Member? = supplier.getMemberOrNull(guildId = guildId, userId = memberId)

    suspend fun getGuild(): Guild = supplier.getGuild(guildId)

    suspend fun getGuildOrNull(): Guild? = supplier.getGuildOrNull(guildId)

    override fun withStrategy(strategy: EntitySupplyStrategy<*>): MemberUpdateEvent =
            MemberUpdateEvent(
                    old,
                    guildId,
                    memberId,
                    currentRoleIds,
                    currentNickName,
                    premiumSince,
                    kord,
                    shard,
                    strategy.supply(kord)
            )
}

package com.gitlab.kordlib.core.event.message

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.GuildBehavior
import com.gitlab.kordlib.core.behavior.channel.MessageChannelBehavior
import com.gitlab.kordlib.core.entity.Guild
import com.gitlab.kordlib.core.entity.Message
import com.gitlab.kordlib.core.entity.Strategizable
import com.gitlab.kordlib.core.entity.channel.MessageChannel
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.core.supplier.getChannelOf
import com.gitlab.kordlib.core.supplier.getChannelOfOrNull

class MessageBulkDeleteEvent(
        val messageIds: Set<Snowflake>,
        val messages: Set<Message>,
        val channelId: Snowflake,
        val guildId: Snowflake?,
        override val kord: Kord,
        override val shard: Int,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : Event, Strategizable {

    val channel: MessageChannelBehavior get() = MessageChannelBehavior(channelId, kord)

    val guild: GuildBehavior? get() = guildId?.let { GuildBehavior(it, kord) }

    suspend fun getChannel(): MessageChannel = supplier.getChannelOf(channelId)

    suspend fun getChannelOrNull(): MessageChannel? = supplier.getChannelOfOrNull(channelId)

    suspend fun getGuild(): Guild? = guildId?.let { supplier.getGuildOrNull(it) }

    override fun withStrategy(strategy: EntitySupplyStrategy<*>): MessageBulkDeleteEvent =
            MessageBulkDeleteEvent(messageIds, messages, channelId, guildId, kord, shard, strategy.supply(kord))

}

package com.gitlab.kordlib.core.event.message

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.exception.RequestException
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.entity.Guild
import com.gitlab.kordlib.core.entity.Member
import com.gitlab.kordlib.core.entity.Message
import com.gitlab.kordlib.core.entity.Strategizable
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.core.entity.channel.DmChannel

class MessageCreateEvent(
        val message: Message,
        val guildId: Snowflake?,
        val member: Member?,
        override val shard: Int,
        override val supplier: EntitySupplier = message.kord.defaultSupplier
) : Event, Strategizable {
    override val kord: Kord get() = message.kord

    /**
     * Requests to get the guild this message was created in, if it was created in one,
     * returns null if the [Guild] isn't present or the message was a [DM][DmChannel].
     *
     * @throws [RequestException] if anything went wrong during the request.
     */
    suspend fun getGuild(): Guild? = guildId?.let { supplier.getGuildOrNull(it) }

    override fun withStrategy(strategy: EntitySupplyStrategy<*>): Strategizable =
            MessageCreateEvent(message, guildId, member, shard, strategy.supply(message.kord))
}

package com.gitlab.kordlib.core.event.message

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.GuildBehavior
import com.gitlab.kordlib.core.behavior.channel.MessageChannelBehavior
import com.gitlab.kordlib.core.entity.Guild
import com.gitlab.kordlib.core.entity.Message
import com.gitlab.kordlib.core.entity.Strategizable
import com.gitlab.kordlib.core.entity.channel.MessageChannel
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.core.supplier.getChannelOf
import com.gitlab.kordlib.core.supplier.getChannelOfOrNull

class MessageDeleteEvent(
        val messageId: Snowflake,
        val channelId: Snowflake,
        val guildId: Snowflake?,
        val message: Message?,
        override val kord: Kord,
        override val shard: Int,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : Event, Strategizable {

    val channel: MessageChannelBehavior get() = MessageChannelBehavior(channelId, kord)

    val guild: GuildBehavior? get() = guildId?.let { GuildBehavior(it, kord) }

    suspend fun getChannel(): MessageChannel = supplier.getChannelOf(channelId)

    suspend fun getChannelOrNull(): MessageChannel? = supplier.getChannelOfOrNull(channelId)

    suspend fun getGuild(): Guild? = guildId?.let { supplier.getGuildOrNull(it) }

    override fun withStrategy(strategy: EntitySupplyStrategy<*>): MessageDeleteEvent =
            MessageDeleteEvent(messageId, channelId, guildId, message, kord, shard, strategy.supply(kord))
}

package com.gitlab.kordlib.core.event.message

import com.gitlab.kordlib.common.entity.DiscordPartialMessage
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.MessageBehavior
import com.gitlab.kordlib.core.behavior.channel.MessageChannelBehavior
import com.gitlab.kordlib.core.entity.Message
import com.gitlab.kordlib.core.entity.Strategizable
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy

class MessageUpdateEvent (
        val messageId: Snowflake,
        val channelId: Snowflake,
        val new: DiscordPartialMessage,
        val old: Message?,
        override val kord: Kord,
        override val shard: Int,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : Event, Strategizable {

    /**
     * The behavior of the message that was updated.
     */
    val message: MessageBehavior get() = MessageBehavior(messageId = messageId, channelId = channelId, kord = kord)

    /**
     * The behavior of the channel in which the message was updated.
     */
    val channel: MessageChannelBehavior get() = MessageChannelBehavior(id = channelId, kord = kord)

    suspend fun getMessage(): Message = supplier.getMessage(channelId = channelId, messageId = messageId)

    suspend fun getMessageOrNull(): Message? = supplier.getMessageOrNull(channelId = channelId, messageId = messageId)

    override fun withStrategy(strategy: EntitySupplyStrategy<*>): MessageUpdateEvent =
            MessageUpdateEvent(messageId, channelId, new, old, kord, shard, strategy.supply(kord))
}

package com.gitlab.kordlib.core.event.message

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.GuildBehavior
import com.gitlab.kordlib.core.behavior.MemberBehavior
import com.gitlab.kordlib.core.behavior.MessageBehavior
import com.gitlab.kordlib.core.behavior.UserBehavior
import com.gitlab.kordlib.core.behavior.channel.MessageChannelBehavior
import com.gitlab.kordlib.core.entity.*
import com.gitlab.kordlib.core.entity.channel.MessageChannel
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.core.supplier.getChannelOf
import com.gitlab.kordlib.core.supplier.getChannelOfOrNull

class ReactionAddEvent(
        val userId: Snowflake,
        val channelId: Snowflake,
        val messageId: Snowflake,
        val guildId: Snowflake?,
        val emoji: ReactionEmoji,
        override val kord: Kord,
        override val shard: Int,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : Event, Strategizable {

    val channel: MessageChannelBehavior get() = MessageChannelBehavior(channelId, kord)

    val guild: GuildBehavior? get() = guildId?.let { GuildBehavior(it, kord) }

    val message: MessageBehavior get() = MessageBehavior(channelId, messageId, kord)

    val user: UserBehavior get() = UserBehavior(userId, kord)

    val userAsMember: MemberBehavior? get() = guildId?.let { MemberBehavior(it, userId, kord) }

    suspend fun getChannel(): MessageChannel = supplier.getChannelOf(channelId)

    suspend fun getChannelOrNull(): MessageChannel? = supplier.getChannelOfOrNull(channelId)

    suspend fun getGuild(): Guild? = guildId?.let { supplier.getGuildOrNull(it) }

    suspend fun getMessage(): Message = supplier.getMessage(channelId = channelId, messageId = messageId)
    suspend fun getMessageOrNull(): Message? = supplier.getMessageOrNull(channelId = channelId, messageId = messageId)

    suspend fun getUser(): User = supplier.getUser(userId)
    suspend fun getUserOrNull(): User? = supplier.getUserOrNull(userId)

    suspend fun getUserAsMember(): Member? = guildId?.let { supplier.getMemberOrNull(it, userId) }

    override fun withStrategy(strategy: EntitySupplyStrategy<*>): ReactionAddEvent =
            ReactionAddEvent(userId, channelId, messageId, guildId, emoji, kord, shard, strategy.supply(kord))
}

package com.gitlab.kordlib.core.event.message

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.GuildBehavior
import com.gitlab.kordlib.core.behavior.MessageBehavior
import com.gitlab.kordlib.core.behavior.channel.MessageChannelBehavior
import com.gitlab.kordlib.core.entity.Guild
import com.gitlab.kordlib.core.entity.Message
import com.gitlab.kordlib.core.entity.Strategizable
import com.gitlab.kordlib.core.entity.channel.MessageChannel
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.core.supplier.getChannelOf
import com.gitlab.kordlib.core.supplier.getChannelOfOrNull

class ReactionRemoveAllEvent(
        val channelId: Snowflake,
        val messageId: Snowflake,
        val guildId: Snowflake?,
        override val kord: Kord,
        override val shard: Int,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : Event, Strategizable {

    val channel: MessageChannelBehavior get() = MessageChannelBehavior(channelId, kord)

    val guild: GuildBehavior? get() = guildId?.let { GuildBehavior(it, kord) }

    val message: MessageBehavior get() = MessageBehavior(channelId, messageId, kord)

    suspend fun getChannel(): MessageChannel = supplier.getChannelOf(channelId)
    suspend fun getChannelOrNull(): MessageChannel? = supplier.getChannelOfOrNull(channelId)

    suspend fun getGuild(): Guild? = guildId?.let { supplier.getGuildOrNull(it) }

    suspend fun getMessage(): Message = supplier.getMessage(channelId = channelId, messageId = messageId)
    suspend fun getMessageOrNull(): Message? = supplier.getMessageOrNull(channelId = channelId, messageId = messageId)

    override fun withStrategy(strategy: EntitySupplyStrategy<*>): ReactionRemoveAllEvent =
            ReactionRemoveAllEvent(channelId, messageId, guildId, kord, shard, strategy.supply(kord))
}

package com.gitlab.kordlib.core.event.message

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.GuildBehavior
import com.gitlab.kordlib.core.behavior.MessageBehavior
import com.gitlab.kordlib.core.behavior.channel.GuildMessageChannelBehavior
import com.gitlab.kordlib.core.cache.data.ReactionRemoveEmojiData
import com.gitlab.kordlib.core.entity.Guild
import com.gitlab.kordlib.core.entity.Message
import com.gitlab.kordlib.core.entity.ReactionEmoji
import com.gitlab.kordlib.core.entity.Strategizable
import com.gitlab.kordlib.core.entity.channel.GuildMessageChannel
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.core.supplier.getChannelOf
import com.gitlab.kordlib.core.supplier.getChannelOfOrNull

class ReactionRemoveEmojiEvent(
        val data: ReactionRemoveEmojiData,
        override val kord: Kord,
        override val shard: Int,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : Event, Strategizable {

    /**
     * The id of the [GuildMessageChannel].
     */
    val channelId: Snowflake get() = Snowflake(data.channelId)

    val channel: GuildMessageChannelBehavior get() = GuildMessageChannelBehavior(guildId = guildId, id = channelId, kord = kord)

    /**
     * The id of the [Guild].
     */
    val guildId: Snowflake get() = Snowflake(data.guildId)

    val guild: GuildBehavior get() = GuildBehavior(id = guildId, kord = kord)

    /**
     * The id of the message.
     */
    val messageId: Snowflake get() = Snowflake(data.messageId)

    val message: MessageBehavior get() = MessageBehavior(channelId = channelId, messageId = messageId, kord = kord)

    /**
     * The emoji that was removed.
     */
    val emoji: ReactionEmoji get() = ReactionEmoji.from(data.emoji)

    suspend fun getChannel(): GuildMessageChannel = supplier.getChannelOf(channelId)

    suspend fun getChannelOrNull(): GuildMessageChannel? = supplier.getChannelOfOrNull(channelId)

    suspend fun getGuild(): Guild = supplier.getGuild(guildId)

    suspend fun getGuildOrNull(): Guild? = supplier.getGuildOrNull(guildId)

    suspend fun getMessage(): Message = supplier.getMessage(channelId = channelId, messageId = messageId)

    suspend fun getMessageOrNull(): Message? = supplier.getMessageOrNull(channelId = channelId, messageId = messageId)

    override fun withStrategy(strategy: EntitySupplyStrategy<*>): ReactionRemoveAllEvent =
            ReactionRemoveAllEvent(channelId, messageId, guildId, kord, shard, strategy.supply(kord))
}

package com.gitlab.kordlib.core.event.message

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.GuildBehavior
import com.gitlab.kordlib.core.behavior.MemberBehavior
import com.gitlab.kordlib.core.behavior.MessageBehavior
import com.gitlab.kordlib.core.behavior.UserBehavior
import com.gitlab.kordlib.core.behavior.channel.MessageChannelBehavior
import com.gitlab.kordlib.core.entity.*
import com.gitlab.kordlib.core.entity.channel.MessageChannel
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy
import com.gitlab.kordlib.core.supplier.getChannelOf
import com.gitlab.kordlib.core.supplier.getChannelOfOrNull

class ReactionRemoveEvent(
        val userId: Snowflake,
        val channelId: Snowflake,
        val messageId: Snowflake,
        val guildId: Snowflake?,
        val emoji: ReactionEmoji,
        override val kord: Kord,
        override val shard: Int,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : Event, Strategizable {

    val channel: MessageChannelBehavior get() = MessageChannelBehavior(channelId, kord)

    val guild: GuildBehavior? get() = guildId?.let { GuildBehavior(it, kord) }

    val message: MessageBehavior get() = MessageBehavior(channelId, messageId, kord)

    val user: UserBehavior get() = UserBehavior(userId, kord)

    val userAsMember: MemberBehavior? get() = guildId?.let { MemberBehavior(it, userId, kord) }

    suspend fun getChannel(): MessageChannel = supplier.getChannelOf(channelId)

    suspend fun getChannelOrNull(): MessageChannel? = supplier.getChannelOfOrNull(channelId)

    suspend fun getGuild(): Guild? = guildId?.let { supplier.getGuildOrNull(it) }

    suspend fun getMessage(): Message = supplier.getMessage(channelId = channelId, messageId = messageId)

    suspend fun getMessageOrNull(): Message? = supplier.getMessageOrNull(channelId = channelId, messageId = messageId)

    suspend fun getUser(): User = supplier.getUser(userId)

    suspend fun getUserOrNull(): User? = supplier.getUserOrNull(userId)

    suspend fun getUserAsMember(): Member? =
            guildId?.let { supplier.getMemberOrNull(guildId = guildId, userId = userId) }

    override fun withStrategy(strategy: EntitySupplyStrategy<*>): ReactionRemoveEvent =
            ReactionRemoveEvent(userId, channelId, messageId, guildId, emoji, kord, shard, strategy.supply(kord))
}

package com.gitlab.kordlib.core.event.role

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.GuildBehavior
import com.gitlab.kordlib.core.entity.Guild
import com.gitlab.kordlib.core.entity.Role
import com.gitlab.kordlib.core.entity.Strategizable
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy

class RoleCreateEvent (
        val role: Role,
        override val shard: Int,
        override val supplier: EntitySupplier = role.kord.defaultSupplier
) : Event, Strategizable {

    override val kord: Kord get() = role.kord

    val guildId: Snowflake get() = role.guildId

    val guild: GuildBehavior get() = GuildBehavior(guildId, kord)

    suspend fun getGuild(): Guild = supplier.getGuild(guildId)

    suspend fun getGuildOrNull():Guild? = supplier.getGuildOrNull(guildId)

    override fun withStrategy(strategy: EntitySupplyStrategy<*>): RoleCreateEvent =
            RoleCreateEvent(role, shard, strategy.supply(kord))
}

package com.gitlab.kordlib.core.event.role

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.GuildBehavior
import com.gitlab.kordlib.core.entity.Guild
import com.gitlab.kordlib.core.entity.Role
import com.gitlab.kordlib.core.entity.Strategizable
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy

class RoleDeleteEvent(
        val guildId: Snowflake,
        val roleId: Snowflake,
        val role: Role?,
        override val kord: Kord,
        override val shard: Int,
        override val supplier: EntitySupplier = kord.defaultSupplier
) : Event, Strategizable {

    val guild: GuildBehavior get() = GuildBehavior(guildId, kord)

    suspend fun getGuild(): Guild = supplier.getGuild(guildId)

    suspend fun getGuildOrNull():Guild? = supplier.getGuildOrNull(guildId)

    override fun withStrategy(strategy: EntitySupplyStrategy<*>): RoleDeleteEvent =
            RoleDeleteEvent(guildId, roleId, role, kord, shard, strategy.supply(kord))
}

package com.gitlab.kordlib.core.event.role

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.GuildBehavior
import com.gitlab.kordlib.core.entity.Guild
import com.gitlab.kordlib.core.entity.Role
import com.gitlab.kordlib.core.entity.Strategizable
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.supplier.EntitySupplier
import com.gitlab.kordlib.core.supplier.EntitySupplyStrategy

class RoleUpdateEvent(
        val role: Role,
        override val shard: Int,
        override val supplier: EntitySupplier = role.kord.defaultSupplier
) : Event, Strategizable {

    override val kord: Kord get() = role.kord

    val guildId: Snowflake get() = role.guildId

    val guild: GuildBehavior get() = GuildBehavior(guildId, kord)

    suspend fun getGuild(): Guild = supplier.getGuild(guildId)

    suspend fun getGuildOrNull():Guild? = supplier.getGuildOrNull(guildId)

    override fun withStrategy(strategy: EntitySupplyStrategy<*>): RoleUpdateEvent =
            RoleUpdateEvent(role, shard, strategy.supply(kord))
}

package com.gitlab.kordlib.core.gateway.handler

import com.gitlab.kordlib.cache.api.DataCache
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.gateway.MasterGateway
import com.gitlab.kordlib.gateway.Gateway
import kotlinx.coroutines.channels.SendChannel
import com.gitlab.kordlib.core.event.Event as CoreEvent
import com.gitlab.kordlib.gateway.Event as GatewayEvent

abstract class BaseGatewayEventHandler(
        protected val kord: Kord,
        protected val gateway: MasterGateway,
        protected val cache: DataCache,
        protected val coreEventChannel: SendChannel<CoreEvent>
) {

    abstract suspend fun handle(event: GatewayEvent, shard: Int)

}

package com.gitlab.kordlib.core.gateway.handler

import com.gitlab.kordlib.cache.api.DataCache
import com.gitlab.kordlib.cache.api.put
import com.gitlab.kordlib.cache.api.query
import com.gitlab.kordlib.cache.api.remove
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.cache.data.ChannelData
import com.gitlab.kordlib.core.entity.channel.*
import com.gitlab.kordlib.core.event.channel.*
import com.gitlab.kordlib.core.gateway.MasterGateway
import com.gitlab.kordlib.core.toInstant
import com.gitlab.kordlib.core.toSnowflakeOrNull
import com.gitlab.kordlib.gateway.*
import kotlinx.coroutines.channels.SendChannel
import com.gitlab.kordlib.core.event.Event as CoreEvent

@Suppress("EXPERIMENTAL_API_USAGE")
internal class ChannelEventHandler(
        kord: Kord,
        gateway: MasterGateway,
        cache: DataCache,
        coreEventChannel: SendChannel<CoreEvent>
) : BaseGatewayEventHandler(kord, gateway, cache, coreEventChannel) {

    override suspend fun handle(event: Event, shard: Int) = when (event) {
        is ChannelCreate -> handle(event, shard)
        is ChannelUpdate -> handle(event, shard)
        is ChannelDelete -> handle(event, shard)
        is ChannelPinsUpdate -> handle(event, shard)
        is TypingStart -> handle(event, shard)
        else -> Unit
    }

    private suspend fun handle(event: ChannelCreate, shard: Int) {
        val data = ChannelData.from(event.channel)
        cache.put(data)

        val event = when (val channel = Channel.from(data, kord)) {
            is NewsChannel -> NewsChannelCreateEvent(channel, shard)
            is StoreChannel -> StoreChannelCreateEvent(channel, shard)
            is DmChannel -> DMChannelCreateEvent(channel, shard)
            is TextChannel -> TextChannelCreateEvent(channel, shard)
            is VoiceChannel -> VoiceChannelCreateEvent(channel, shard)
            is Category -> CategoryCreateEvent(channel, shard)
            else -> error("unknown channel: $channel")
        }

        coreEventChannel.send(event)
    }

    private suspend fun handle(event: ChannelUpdate, shard: Int) {
        val data = ChannelData.from(event.channel)
        cache.put(data)

        val event = when (val channel = Channel.from(data, kord)) {
            is NewsChannel -> NewsChannelUpdateEvent(channel, shard)
            is StoreChannel -> StoreChannelUpdateEvent(channel, shard)
            is DmChannel -> DMChannelUpdateEvent(channel, shard)
            is TextChannel -> TextChannelUpdateEvent(channel, shard)
            is VoiceChannel -> VoiceChannelUpdateEvent(channel, shard)
            is Category -> CategoryUpdateEvent(channel, shard)
            else -> error("unknown channel: $channel")
        }

        coreEventChannel.send(event)
    }

    private suspend fun handle(event: ChannelDelete, shard: Int) {
        cache.remove<ChannelData> { ChannelData::id eq event.channel.id.toLong() }
        val data = ChannelData.from(event.channel)

        val event = when (val channel = Channel.from(data, kord)) {
            is NewsChannel -> NewsChannelDeleteEvent(channel, shard)
            is StoreChannel -> StoreChannelDeleteEvent(channel, shard)
            is DmChannel -> DMChannelDeleteEvent(channel, shard)
            is TextChannel -> TextChannelDeleteEvent(channel, shard)
            is VoiceChannel -> VoiceChannelDeleteEvent(channel, shard)
            is Category -> CategoryDeleteEvent(channel, shard)
            else -> error("unknown channel: $channel")
        }

        coreEventChannel.send(event)
    }

    private suspend fun handle(event: ChannelPinsUpdate, shard: Int) = with(event.pins) {
        val event = ChannelPinsUpdateEvent(Snowflake(channelId), lastPinTimestamp?.toInstant(), kord, shard)

        cache.query<ChannelData> { ChannelData::id eq channelId.toLong() }.update {
            it.copy(lastPinTimestamp = lastPinTimestamp ?: it.lastPinTimestamp)
        }

        coreEventChannel.send(event)
    }

    private suspend fun handle(event: TypingStart, shard: Int) = with(event.data) {
        val event = TypingStartEvent(
                Snowflake(channelId),
                Snowflake(userId),
                guildId.toSnowflakeOrNull(),
                timestamp.toInstant(),
                kord,
                shard
        )

        coreEventChannel.send(event)
    }

}

package com.gitlab.kordlib.core.gateway.handler

import com.gitlab.kordlib.cache.api.DataCache
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.gateway.MasterGateway
import com.gitlab.kordlib.gateway.Event
import io.ktor.util.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import mu.KotlinLogging
import com.gitlab.kordlib.core.event.Event as CoreEvent

private val logger = KotlinLogging.logger { }

@Suppress("EXPERIMENTAL_API_USAGE")
class GatewayEventInterceptor(
        private val kord: Kord,
        private val gateway: MasterGateway,
        cache: DataCache,
        coreEventChannel: SendChannel<CoreEvent>,
) {

    private val listeners = listOf(
            MessageEventHandler(kord, gateway, cache, coreEventChannel),
            ChannelEventHandler(kord, gateway, cache, coreEventChannel),
            GuildEventHandler(kord, gateway, cache, coreEventChannel),
            LifeCycleEventHandler(kord, gateway, cache, coreEventChannel),
            UserEventHandler(kord, gateway, cache, coreEventChannel),
            VoiceEventHandler(kord, gateway, cache, coreEventChannel),
            WebhookEventHandler(kord, gateway, cache, coreEventChannel)
    )

    suspend fun start() = gateway.events
                .buffer(Channel.UNLIMITED)
                .onEach { (event, _, shard) -> dispatch(event, shard) }
                .launchIn(kord)

    private suspend fun dispatch(event: Event, shard: Int) {
        runCatching {
            listeners.forEach { it.handle(event, shard) }
        }.onFailure {
            logger.error(it)
        }
    }

}

package com.gitlab.kordlib.core.gateway.handler

import com.gitlab.kordlib.cache.api.DataCache
import com.gitlab.kordlib.cache.api.put
import com.gitlab.kordlib.cache.api.putAll
import com.gitlab.kordlib.cache.api.query
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.cache.data.*
import com.gitlab.kordlib.core.entity.*
import com.gitlab.kordlib.core.entity.Presence
import com.gitlab.kordlib.core.event.PresenceUpdateEvent
import com.gitlab.kordlib.core.event.guild.*
import com.gitlab.kordlib.core.event.role.RoleCreateEvent
import com.gitlab.kordlib.core.event.role.RoleDeleteEvent
import com.gitlab.kordlib.core.event.role.RoleUpdateEvent
import com.gitlab.kordlib.core.gateway.MasterGateway
import com.gitlab.kordlib.core.toInstant
import com.gitlab.kordlib.gateway.*
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.singleOrNull
import kotlinx.coroutines.flow.toSet
import com.gitlab.kordlib.common.entity.DiscordGuild as GatewayGuild
import com.gitlab.kordlib.core.event.Event as CoreEvent

@Suppress("EXPERIMENTAL_API_USAGE")
internal class GuildEventHandler(
        kord: Kord,
        gateway: MasterGateway,
        cache: DataCache,
        coreEventChannel: SendChannel<CoreEvent>
) : BaseGatewayEventHandler(kord, gateway, cache, coreEventChannel) {

    override suspend fun handle(event: Event, shard: Int) = when (event) {
        is GuildCreate -> handle(event, shard)
        is GuildUpdate -> handle(event, shard)
        is GuildDelete -> handle(event, shard)
        is GuildBanAdd -> handle(event, shard)
        is GuildBanRemove -> handle(event, shard)
        is GuildEmojisUpdate -> handle(event, shard)
        is GuildIntegrationsUpdate -> handle(event, shard)
        is GuildMemberAdd -> handle(event, shard)
        is GuildMemberRemove -> handle(event, shard)
        is GuildMemberUpdate -> handle(event, shard)
        is GuildRoleCreate -> handle(event, shard)
        is GuildRoleUpdate -> handle(event, shard)
        is GuildRoleDelete -> handle(event, shard)
        is GuildMembersChunk -> handle(event, shard)
        is PresenceUpdate -> handle(event, shard)
        is InviteCreate -> handle(event, shard)
        is InviteDelete -> handle(event, shard)
        else -> Unit
    }

    private suspend fun GatewayGuild.cache() {
        for (member in members.orEmpty()) {
            cache.put(MemberData.from(member.user!!.id, id, member))
            cache.put(UserData.from(member.user!!))
        }

        for (role in roles) {
            cache.put(RoleData.from(id, role))
        }

        for (channel in channels.orEmpty()) {
            cache.put(ChannelData.from(channel.copy(guildId = this.id))) //guild id always empty
        }

        for (presence in presences.orEmpty()) {
            cache.put(PresenceData.from(id, presence))
        }

        for (voiceState in voiceStates.orEmpty()) {
            cache.put(VoiceStateData.from(id, voiceState))
        }
        for (emoji in emojis) {
            cache.put(EmojiData.from(id, emoji.id!!, emoji))
        }
    }

    private suspend fun handle(event: GuildCreate, shard: Int) {
        val data = GuildData.from(event.guild)
        cache.put(data)
        event.guild.cache()

        coreEventChannel.send(GuildCreateEvent(Guild(data, kord), shard))
    }

    private suspend fun handle(event: GuildUpdate, shard: Int) {
        val data = GuildData.from(event.guild)
        cache.put(data)
        event.guild.cache()

        coreEventChannel.send(GuildCreateEvent(Guild(data, kord), shard))
    }

    private suspend fun handle(event: GuildDelete, shard: Int) = with(event.guild) {
        val query = cache.query<GuildData> { GuildData::id eq id.toLong() }

        val old = query.asFlow().map { Guild(it, kord) }.singleOrNull()
        query.remove()

        coreEventChannel.send(GuildDeleteEvent(Snowflake(id), unavailable ?: false, old, kord, shard))
    }

    private suspend fun handle(event: GuildBanAdd, shard: Int) = with(event.ban) {
        val data = UserData.from(user)
        cache.put(user)
        val user = User(data, kord)

        coreEventChannel.send(BanAddEvent(user, Snowflake(guildId), shard))
    }

    private suspend fun handle(event: GuildBanRemove, shard: Int) = with(event.ban) {
        val data = UserData.from(user)
        cache.put(user)
        val user = User(data, kord)

        coreEventChannel.send(BanRemoveEvent(user, Snowflake(guildId), shard))
    }

    private suspend fun handle(event: GuildEmojisUpdate, shard: Int) = with(event.emoji) {
        val guildId = Snowflake(guildId)
        val emojis = emojis.map { GuildEmoji(EmojiData.from(guildId.value, it.id!!, it), kord) }.toSet()

        cache.query<GuildData> { GuildData::id eq guildId.longValue }.update {
            it.copy(emojis = emojis.map { emoji -> emoji.data })
        }

        coreEventChannel.send(EmojisUpdateEvent(guildId, emojis, kord, shard))
    }

    private suspend fun handle(event: GuildIntegrationsUpdate, shard: Int) {
        coreEventChannel.send(IntegrationsUpdateEvent(Snowflake(event.integrations.guildId), kord, shard))
    }

    private suspend fun handle(event: GuildMemberAdd, shard: Int) = with(event.member) {
        val userData = UserData.from(user!!)
        val memberData = MemberData.from(user!!.id, event.member)

        cache.put(userData)
        cache.put(memberData)

        val member = Member(memberData, userData, kord)

        coreEventChannel.send(MemberJoinEvent(member, shard))
    }

    private suspend fun handle(event: GuildMemberRemove, shard: Int) = with(event.member) {
        val userData = UserData.from(user)
        cache.query<UserData> { UserData::id eq userData.id }.remove()
        val user = User(userData, kord)

        coreEventChannel.send(MemberLeaveEvent(user, Snowflake(guildId), shard))
    }

    private suspend fun handle(event: GuildMemberUpdate, shard: Int) = with(event.member) {
        val userData = UserData.from(user)
        cache.put(userData)

        val old = cache.query<MemberData> {
            MemberData::userId eq userData.id
            MemberData::guildId eq guildId.toLong()
        }.asFlow().map { Member(it, userData, kord) }.singleOrNull()

        cache.query<MemberData> {
            MemberData::userId eq userData.id
            MemberData::guildId eq guildId.toLong()
        }.update { it + this }

        val roles = roles.asSequence().map { Snowflake(it) }.toSet()

        coreEventChannel.send(
                MemberUpdateEvent(
                        old,
                        Snowflake(guildId),
                        Snowflake(userData.id),
                        roles,
                        nick ?: userData.username,
                        premiumSince?.toInstant(),
                        kord,
                        shard
                )
        )
    }

    private suspend fun handle(event: GuildRoleCreate, shard: Int) {
        val data = RoleData.from(event.role)
        cache.put(data)

        coreEventChannel.send(RoleCreateEvent(Role(data, kord), shard))
    }

    private suspend fun handle(event: GuildRoleUpdate, shard: Int) {
        val data = RoleData.from(event.role)
        cache.put(data)

        coreEventChannel.send(RoleUpdateEvent(Role(data, kord), shard))
    }

    private suspend fun handle(event: GuildRoleDelete, shard: Int) = with(event.role) {
        val query = cache.query<RoleData> { RoleData::id eq event.role.id.toLong() }

        val old = run {
            val data = query.singleOrNull() ?: return@run null
            Role(data, kord)
        }

        query.remove()

        coreEventChannel.send(RoleDeleteEvent(Snowflake(guildId), Snowflake(id), old, kord, shard))
    }

    private suspend fun handle(event: GuildMembersChunk, shard: Int) = with(event.data) {
        val presences = presences.orEmpty().map { PresenceData.from(guildId, it) }
        cache.putAll(presences)

        val members = members.asFlow().map { member ->
            val memberData = MemberData.from(member.user!!.id, guildId, member)
            cache.put(memberData)
            val userData = UserData.from(member.user!!)
            cache.put(userData)

            Member(memberData, userData, kord)
        }.toSet()

        coreEventChannel.send(MemberChunksEvent(Snowflake(guildId), members, kord, shard))
    }

    private suspend fun handle(event: PresenceUpdate, shard: Int) = with(event.presence) {
        val data = PresenceData.from(this.guildId!!, this)

        val old = cache.query<PresenceData> { PresenceData::id eq data.id }
                .asFlow().map { Presence(it, kord) }.singleOrNull()

        cache.put(data)
        val new = Presence(data, kord)

        val user = cache
                .query<UserData> { UserData::id eq event.presence.user.id.toLong() }
                .singleOrNull()
                ?.let { User(it, kord) }

        coreEventChannel.send(PresenceUpdateEvent(user, this.user, Snowflake(guildId!!), old, new, shard))
    }

    private suspend fun handle(event: InviteCreate, shard: Int) = with(event) {
        val data = InviteCreateData.from(invite)

        with(invite.inviter) {
            cache.query<UserData> { UserData::id eq id.toLong() }
                    .update { it.copy(discriminator = discriminator, username = username, avatar = avatar) }
        }

        coreEventChannel.send(InviteCreateEvent(data, kord, shard))
    }

    private suspend fun handle(event: InviteDelete, shard: Int) = with(event) {
        val data = InviteDeleteData.from(invite)
        coreEventChannel.send(InviteDeleteEvent(data, kord, shard))
    }

}

package com.gitlab.kordlib.core.gateway.handler

import com.gitlab.kordlib.cache.api.DataCache
import com.gitlab.kordlib.cache.api.put
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.cache.data.UserData
import com.gitlab.kordlib.core.entity.User
import com.gitlab.kordlib.core.event.gateway.ConnectEvent
import com.gitlab.kordlib.core.event.gateway.DisconnectEvent
import com.gitlab.kordlib.core.event.gateway.ReadyEvent
import com.gitlab.kordlib.core.event.gateway.ResumedEvent
import com.gitlab.kordlib.core.gateway.MasterGateway
import com.gitlab.kordlib.gateway.*
import kotlinx.coroutines.channels.SendChannel
import com.gitlab.kordlib.core.event.Event as CoreEvent

@Suppress("EXPERIMENTAL_API_USAGE")
internal class LifeCycleEventHandler(
        kord: Kord,
        gateway: MasterGateway,
        cache: DataCache,
        coreEventChannel: SendChannel<CoreEvent>
) : BaseGatewayEventHandler(kord, gateway, cache, coreEventChannel) {

    override suspend fun handle(event: Event, shard: Int) = when (event) {
        is Ready -> handle(event, shard)
        is Resumed -> coreEventChannel.send(ResumedEvent(kord, shard))
        Reconnect -> coreEventChannel.send(ConnectEvent(kord, shard))
        is Close -> when (event) {
            Close.Detach -> coreEventChannel.send(DisconnectEvent.DetachEvent(kord, shard))
            Close.UserClose -> coreEventChannel.send(DisconnectEvent.UserCloseEvent(kord, shard))
            Close.Timeout -> coreEventChannel.send(DisconnectEvent.TimeoutEvent(kord, shard))
            is Close.DiscordClose -> coreEventChannel.send(DisconnectEvent.DiscordCloseEvent(kord, shard, event.closeCode, event.recoverable))
            Close.Reconnecting -> coreEventChannel.send(DisconnectEvent.ReconnectingEvent(kord, shard))
            Close.ZombieConnection -> coreEventChannel.send(DisconnectEvent.ZombieConnectionEvent(kord, shard))
            Close.RetryLimitReached -> coreEventChannel.send(DisconnectEvent.RetryLimitReachedEvent(kord, shard))
            Close.SessionReset -> coreEventChannel.send(DisconnectEvent.SessionReset(kord, shard))
        }

        else -> Unit
    }

    private suspend fun handle(event: Ready, shard: Int) = with(event.data) {
        val guilds = guilds.map { Snowflake(it.id) }.toSet()
        val self = UserData.from(event.data.user)

        cache.put(self)

        coreEventChannel.send(ReadyEvent(event.data.version, guilds, User(self, kord), sessionId, kord, shard))
    }
}

package com.gitlab.kordlib.core.gateway.handler

import com.gitlab.kordlib.cache.api.DataCache
import com.gitlab.kordlib.cache.api.put
import com.gitlab.kordlib.cache.api.query
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.cache.data.*
import com.gitlab.kordlib.core.entity.Member
import com.gitlab.kordlib.core.entity.Message
import com.gitlab.kordlib.core.entity.ReactionEmoji
import com.gitlab.kordlib.core.event.message.*
import com.gitlab.kordlib.core.gateway.MasterGateway
import com.gitlab.kordlib.core.toSnowflakeOrNull
import com.gitlab.kordlib.gateway.*
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.singleOrNull
import kotlinx.coroutines.flow.toSet
import com.gitlab.kordlib.core.event.Event as CoreEvent

@Suppress("EXPERIMENTAL_API_USAGE")
internal class MessageEventHandler(
        kord: Kord,
        gateway: MasterGateway,
        cache: DataCache,
        coreEventChannel: SendChannel<CoreEvent>
) : BaseGatewayEventHandler(kord, gateway, cache, coreEventChannel) {

    override suspend fun handle(event: Event, shard: Int) = when (event) {
        is MessageCreate -> handle(event, shard)
        is MessageUpdate -> handle(event, shard)
        is MessageDelete -> handle(event, shard)
        is MessageDeleteBulk -> handle(event, shard)
        is MessageReactionAdd -> handle(event, shard)
        is MessageReactionRemove -> handle(event, shard)
        is MessageReactionRemoveAll -> handle(event, shard)
        is MessageReactionRemoveEmoji -> handle(event, shard)
        else -> Unit
    }

    private suspend fun handle(event: MessageCreate, shard: Int) = with(event.message) {
        val data = MessageData.from(this)
        cache.put(data)

        cache.query<ChannelData> { ChannelData::id eq channelId.toLong() }.update {
            it.copy(lastMessageId = data.id)
        }

        //get the user data only if it exists and the user isn't a webhook
        val userData =  if (author != null && webhookId == null) {
            UserData.from(author!!).also { cache.put(it) }
        } else null

        //get the member and cache the member. We need the user, guild id and member to be present
        val member = if (userData != null && guildId != null && member != null) {
            val memberData = MemberData.from(author!!.id, guildId!!, member!!)
            cache.put(memberData)
            Member(memberData, userData, kord)
        } else null

        coreEventChannel.send(MessageCreateEvent(Message(data, kord), guildId.toSnowflakeOrNull(), member, shard))
    }

    private suspend fun handle(event: MessageUpdate, shard: Int) = with(event.message) {
        val query = cache.query<MessageData> { MessageData::id eq id.toLong() }

        val old = query.asFlow().map { Message(it, kord) }.singleOrNull()
        query.update { it + this }

        coreEventChannel.send(MessageUpdateEvent(Snowflake(id), Snowflake(channelId), this, old, kord, shard))
    }

    private suspend fun handle(event: MessageDelete, shard: Int) = with(event.message) {
        val query = cache.query<MessageData> { MessageData::id eq id.toLong() }

        val removed = query.singleOrNull()?.let { Message(it, kord) }
        query.remove()

        coreEventChannel.send(
                MessageDeleteEvent(Snowflake(id), Snowflake(channelId), guildId.toSnowflakeOrNull(), removed, kord, shard)
        )
    }

    private suspend fun handle(event: MessageDeleteBulk, shard: Int) = with(event.messageBulk) {
        val query = cache.query<MessageData> { MessageData::id `in` ids }

        val removed = query.asFlow().map { Message(it, kord) }.toSet()
        query.remove()

        val ids = ids.asSequence().map { Snowflake(it) }.toSet()

        coreEventChannel.send(
                MessageBulkDeleteEvent(ids, removed, Snowflake(channelId), guildId.toSnowflakeOrNull(), kord, shard)
        )
    }

    private suspend fun handle(event: MessageReactionAdd, shard: Int) = with(event.reaction) {
        /**
         * Reactions added will *always* have a name, the only case in which name is null is when a guild reaction
         * no longer exists (only id is kept). Reacting with a non-existing reaction *should* be impossible.
         **/
        val reaction = when (val id = emoji.id) {
            null -> ReactionEmoji.Unicode(emoji.name!!)
            else -> ReactionEmoji.Custom(Snowflake(id), emoji.name!!, emoji.animated ?: false)
        }

        cache.query<MessageData> { MessageData::id eq messageId.toLong() }.update {
            val isMe = kord.selfId.value == event.reaction.userId

            val reactions = if (it.reactions.isNullOrEmpty()) {
                listOf(ReactionData.from(1, isMe, emoji))
            } else {
                val reactions = it.reactions.orEmpty()
                val reaction = reactions.firstOrNull { reaction ->
                    if (emoji.id == null) reaction.emojiName == emoji.name
                    else reaction.emojiId?.toString() == emoji.id && reaction.emojiName == emoji.name
                }

                when (reaction) {
                    null -> reactions + ReactionData.from(1, isMe, emoji)
                    else -> (reactions - reaction) + reaction.copy(count = reaction.count + 1, me = isMe)
                }
            }

            it.copy(reactions = reactions)
        }

        coreEventChannel.send(
                ReactionAddEvent(
                        Snowflake(userId),
                        Snowflake(channelId),
                        Snowflake(messageId),
                        guildId.toSnowflakeOrNull(),
                        reaction,
                        kord,
                        shard
                )
        )
    }

    private suspend fun handle(event: MessageReactionRemove, shard: Int) = with(event.reaction) {
        /**
         * Reactions removed will *sometimes* have a name, the only case in which name is null is when a guild reaction
         * no longer exists (only id is kept). Reomving a non-existing reaction *should* be possible.
         **/
        val reaction = when (val id = emoji.id) {
            null -> ReactionEmoji.Unicode(emoji.name!!)
            else -> ReactionEmoji.Custom(Snowflake(id), emoji.name ?: "", emoji.animated ?: false)
        }

        cache.query<MessageData> { MessageData::id eq messageId.toLong() }.update {
            if (it.reactions.isNullOrEmpty()) return@update it

            val me = kord.selfId.value == event.reaction.userId

            val oldReactions = it.reactions.orEmpty()
            val reaction = oldReactions.firstOrNull { reaction ->
                if (emoji.id == null) reaction.emojiName == emoji.name
                else reaction.emojiId?.toString() == emoji.id && reaction.emojiName == emoji.name
            } ?: return@update it

            val reactions = when (val count = reaction.count - 1) {
                0 -> (oldReactions - reaction)
                else -> (oldReactions - reaction) + reaction.copy(count = count, me = reaction.me xor me)
            }

            it.copy(reactions = reactions)
        }

        coreEventChannel.send(
                ReactionRemoveEvent(
                        Snowflake(userId),
                        Snowflake(channelId),
                        Snowflake(messageId),
                        guildId.toSnowflakeOrNull(),
                        reaction,
                        kord,
                        shard
                )
        )
    }

    private suspend fun handle(event: MessageReactionRemoveAll, shard: Int) = with(event.reactions) {
        cache.query<MessageData> { MessageData::id eq messageId.toLong() }.update { it.copy(reactions = emptyList()) }

        coreEventChannel.send(
                ReactionRemoveAllEvent(
                        Snowflake(channelId),
                        Snowflake(messageId),
                        guildId.toSnowflakeOrNull(),
                        kord,
                        shard
                )
        )
    }

    private suspend fun handle(event: MessageReactionRemoveEmoji, shard: Int) = with(event.reaction) {
        cache.query<MessageData> { MessageData::id eq messageId.toLong() }.update { it.copy(reactions = it.reactions?.filter { it.emojiName != emoji.name }) }

        val data = ReactionRemoveEmojiData.from(this)
        coreEventChannel.send(ReactionRemoveEmojiEvent(data, kord, shard))
    }

}

package com.gitlab.kordlib.core.gateway.handler

import com.gitlab.kordlib.cache.api.DataCache
import com.gitlab.kordlib.cache.api.put
import com.gitlab.kordlib.cache.api.query
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.cache.data.UserData
import com.gitlab.kordlib.core.entity.User
import com.gitlab.kordlib.core.event.UserUpdateEvent
import com.gitlab.kordlib.core.gateway.MasterGateway
import com.gitlab.kordlib.gateway.Event
import com.gitlab.kordlib.gateway.Gateway
import com.gitlab.kordlib.gateway.UserUpdate
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.singleOrNull
import com.gitlab.kordlib.core.event.Event as CoreEvent

@Suppress("EXPERIMENTAL_API_USAGE")
internal class UserEventHandler(
        kord: Kord,
        gateway: MasterGateway,
        cache: DataCache,
        coreEventChannel: SendChannel<CoreEvent>
) : BaseGatewayEventHandler(kord, gateway, cache, coreEventChannel) {

    override suspend fun handle(event: Event, shard: Int) = when (event) {
        is UserUpdate -> handle(event, shard)
        else -> Unit
    }

    private suspend fun handle(event: UserUpdate, shard: Int) {
        val data = UserData.from(event.user)

        val old = cache.query<UserData> { UserData::id eq data.id }
                .asFlow().map { User(it, kord) }.singleOrNull()

        cache.put(data)
        val new = User(data, kord)

        coreEventChannel.send(UserUpdateEvent(old, new, shard))
    }

}

package com.gitlab.kordlib.core.gateway.handler

import com.gitlab.kordlib.cache.api.DataCache
import com.gitlab.kordlib.cache.api.put
import com.gitlab.kordlib.cache.api.query
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.cache.data.*
import com.gitlab.kordlib.core.entity.VoiceState
import com.gitlab.kordlib.core.event.VoiceServerUpdateEvent
import com.gitlab.kordlib.core.event.VoiceStateUpdateEvent
import com.gitlab.kordlib.core.gateway.MasterGateway
import com.gitlab.kordlib.gateway.Event
import com.gitlab.kordlib.gateway.Gateway
import com.gitlab.kordlib.gateway.VoiceServerUpdate
import com.gitlab.kordlib.gateway.VoiceStateUpdate
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.singleOrNull
import com.gitlab.kordlib.core.event.Event as CoreEvent

@Suppress("EXPERIMENTAL_API_USAGE")
internal class VoiceEventHandler(
        kord: Kord,
        gateway: MasterGateway,
        cache: DataCache,
        coreEventChannel: SendChannel<CoreEvent>
) : BaseGatewayEventHandler(kord, gateway, cache, coreEventChannel) {

    override suspend fun handle(event: Event, shard: Int) = when (event) {
        is VoiceStateUpdate -> handle(event, shard)
        is VoiceServerUpdate -> handle(event, shard)
        else -> Unit
    }

    private suspend fun handle(event: VoiceStateUpdate, shard: Int) {
        val data = VoiceStateData.from(event.voiceState.guildId, event.voiceState)

        val old = cache.query<VoiceStateData> { VoiceStateData::id eq data.id }
                .asFlow().map { VoiceState(it, kord) }.singleOrNull()

        cache.put(data)
        val new = VoiceState(data, kord)

        coreEventChannel.send( VoiceStateUpdateEvent(old, new, shard))
    }

    private suspend fun handle(event: VoiceServerUpdate, shard: Int) = with(event.voiceServerUpdateData) {
        coreEventChannel.send(VoiceServerUpdateEvent(token, Snowflake(guildId), endpoint, kord, shard))
    }

}

package com.gitlab.kordlib.core.gateway.handler

import com.gitlab.kordlib.cache.api.DataCache
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.event.WebhookUpdateEvent
import com.gitlab.kordlib.core.gateway.MasterGateway
import com.gitlab.kordlib.gateway.Event
import com.gitlab.kordlib.gateway.Gateway
import com.gitlab.kordlib.gateway.WebhooksUpdate
import kotlinx.coroutines.channels.SendChannel
import com.gitlab.kordlib.core.event.Event as CoreEvent

@Suppress("EXPERIMENTAL_API_USAGE")
internal class WebhookEventHandler(
        kord: Kord,
        gateway: MasterGateway,
        cache: DataCache,
        coreEventChannel: SendChannel<CoreEvent>
) : BaseGatewayEventHandler(kord, gateway, cache, coreEventChannel) {

    override suspend fun handle(event: Event, shard: Int) = when (event) {
        is WebhooksUpdate -> handle(event, shard)
        else -> Unit
    }

    private suspend fun handle(event: WebhooksUpdate, shard: Int) = with(event.webhooksUpdateData) {
        coreEventChannel.send(WebhookUpdateEvent(Snowflake(guildId), Snowflake(channelId), kord, shard))
    }

}

package com.gitlab.kordlib.core.live.channel

import com.gitlab.kordlib.common.annotation.KordPreview
import com.gitlab.kordlib.core.entity.Entity
import com.gitlab.kordlib.core.entity.channel.Category
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.event.channel.CategoryCreateEvent
import com.gitlab.kordlib.core.event.channel.CategoryDeleteEvent
import com.gitlab.kordlib.core.event.channel.CategoryUpdateEvent
import com.gitlab.kordlib.core.event.guild.GuildDeleteEvent

@KordPreview
fun Category.live() = LiveCategory(this)

@KordPreview
class LiveCategory(channel: Category) : LiveChannel(), Entity by channel {

    override var channel: Category = channel
        private set

    override fun update(event: Event) = when (event) {
        is CategoryCreateEvent -> channel = event.channel
        is CategoryUpdateEvent -> channel = event.channel
        is CategoryDeleteEvent -> shutDown()

        is GuildDeleteEvent -> shutDown()

        else -> Unit
    }

}

package com.gitlab.kordlib.core.live.channel

import com.gitlab.kordlib.common.annotation.KordPreview
import com.gitlab.kordlib.core.entity.channel.*
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.event.VoiceStateUpdateEvent
import com.gitlab.kordlib.core.event.channel.ChannelCreateEvent
import com.gitlab.kordlib.core.event.channel.ChannelDeleteEvent
import com.gitlab.kordlib.core.event.channel.ChannelUpdateEvent
import com.gitlab.kordlib.core.event.guild.GuildCreateEvent
import com.gitlab.kordlib.core.event.guild.GuildDeleteEvent
import com.gitlab.kordlib.core.event.guild.GuildUpdateEvent
import com.gitlab.kordlib.core.event.message.*
import com.gitlab.kordlib.core.live.AbstractLiveEntity

@KordPreview
fun Channel.live() = when (this) {
    is DmChannel -> this.live()
    is NewsChannel -> this.live()
    is StoreChannel -> this.live()
    is TextChannel -> this.live()
    is VoiceChannel -> this.live()
    else -> error("unsupported channel type")
}

@KordPreview
abstract class LiveChannel : AbstractLiveEntity() {

    abstract val channel: Channel

    override fun filter(event: Event): Boolean = when (event) {
        is VoiceStateUpdateEvent -> event.state.channelId == channel.id

        is ReactionAddEvent -> event.channelId == channel.id
        is ReactionRemoveEvent -> event.channelId == channel.id
        is ReactionRemoveAllEvent -> event.channelId == channel.id

        is MessageCreateEvent -> event.message.channelId == channel.id
        is MessageUpdateEvent -> event.new.channelId == channel.id.value
        is MessageDeleteEvent -> event.channelId == channel.id

        is ChannelCreateEvent -> event.channel.id == channel.id
        is ChannelUpdateEvent -> event.channel.id == channel.id
        is ChannelDeleteEvent -> event.channel.id == channel.id

        is GuildCreateEvent -> event.guild.id.longValue == channel.data.guildId
        is GuildUpdateEvent -> event.guild.id.longValue == channel.data.guildId
        is GuildDeleteEvent -> event.guildId.longValue == channel.data.guildId

        else -> false
    }

}

package com.gitlab.kordlib.core.live.channel

import com.gitlab.kordlib.common.annotation.KordPreview
import com.gitlab.kordlib.core.entity.Entity
import com.gitlab.kordlib.core.entity.channel.DmChannel
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.event.channel.DMChannelCreateEvent
import com.gitlab.kordlib.core.event.channel.DMChannelDeleteEvent
import com.gitlab.kordlib.core.event.channel.DMChannelUpdateEvent
import com.gitlab.kordlib.core.event.guild.GuildDeleteEvent

@KordPreview
fun DmChannel.live() = LiveDmChannel(this)

@KordPreview
class LiveDmChannel(channel: DmChannel) : LiveChannel(), Entity by channel {

    override var channel: DmChannel = channel
        private set

    override fun update(event: Event) = when (event) {
        is DMChannelCreateEvent -> channel = event.channel
        is DMChannelUpdateEvent -> channel = event.channel
        is DMChannelDeleteEvent -> shutDown()

        is GuildDeleteEvent -> shutDown()

        else -> Unit
    }

}

package com.gitlab.kordlib.core.live.channel

import com.gitlab.kordlib.common.annotation.KordPreview
import com.gitlab.kordlib.core.entity.Entity
import com.gitlab.kordlib.core.entity.channel.GuildChannel
import com.gitlab.kordlib.core.entity.channel.GuildMessageChannel
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.event.channel.ChannelCreateEvent
import com.gitlab.kordlib.core.event.channel.ChannelDeleteEvent
import com.gitlab.kordlib.core.event.channel.ChannelUpdateEvent
import com.gitlab.kordlib.core.event.guild.GuildDeleteEvent

@KordPreview
fun GuildChannel.live() = LiveGuildChannel(this)

@KordPreview
class LiveGuildChannel(channel: GuildChannel) : LiveChannel(), Entity by channel {

    override var channel: GuildChannel = channel
        private set

    override fun update(event: Event) = when (event) {
        is ChannelCreateEvent -> channel = event.channel as GuildMessageChannel
        is ChannelUpdateEvent -> channel = event.channel as GuildMessageChannel
        is ChannelDeleteEvent -> shutDown()

        is GuildDeleteEvent -> shutDown()

        else -> Unit
    }

}

package com.gitlab.kordlib.core.live.channel

import com.gitlab.kordlib.common.annotation.KordPreview
import com.gitlab.kordlib.core.entity.Entity
import com.gitlab.kordlib.core.entity.channel.GuildMessageChannel
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.event.channel.*
import com.gitlab.kordlib.core.event.guild.GuildDeleteEvent

@KordPreview
fun GuildMessageChannel.live() = LiveGuildMessageChannel(this)

@KordPreview
class LiveGuildMessageChannel(channel: GuildMessageChannel) : LiveChannel(), Entity by channel {

    override var channel: GuildMessageChannel = channel
        private set

    override fun update(event: Event) = when (event) {
        is ChannelCreateEvent -> channel = event.channel as GuildMessageChannel
        is ChannelUpdateEvent -> channel = event.channel as GuildMessageChannel
        is ChannelDeleteEvent -> shutDown()

        is GuildDeleteEvent -> shutDown()

        else -> Unit
    }

}

package com.gitlab.kordlib.core.live.channel

import com.gitlab.kordlib.common.annotation.KordPreview
import com.gitlab.kordlib.core.entity.Entity
import com.gitlab.kordlib.core.entity.channel.VoiceChannel
import com.gitlab.kordlib.core.event.Event
import com.gitlab.kordlib.core.event.channel.VoiceChannelCreateEvent
import com.gitlab.kordlib.core.event.channel.VoiceChannelDeleteEvent
import com.gitlab.kordlib.core.event.channel.VoiceChannelUpdateEvent
import com.gitlab.kordlib.core.event.guild.GuildDeleteEvent

@KordPreview
fun VoiceChannel.live() = LiveVoiceChannel(this)

@KordPreview
class LiveVoiceChannel(channel: VoiceChannel) : LiveChannel(), Entity by channel {

    override var channel: VoiceChannel = channel
        private set

    override fun update(event: Event) = when (event) {
        is VoiceChannelCreateEvent -> channel = event.channel
        is VoiceChannelUpdateEvent -> channel = event.channel
        is VoiceChannelDeleteEvent -> shutDown()

        is GuildDeleteEvent -> shutDown()

        else -> Unit
    }

}

package com.gitlab.kordlib.rest.builder.ban

import com.gitlab.kordlib.rest.builder.AuditRequestBuilder
import com.gitlab.kordlib.common.annotation.KordDsl
import com.gitlab.kordlib.rest.json.request.GuildBanAddRequest

@KordDsl
class BanCreateBuilder : AuditRequestBuilder<GuildBanAddRequest> {
    /**
     * The reason for banning this member.
     */
    override var reason: String? = null

    /**
     * The number of days to delete messages for (0-7).
     */
    var deleteMessagesDays: Int? = null

    override fun toRequest() = GuildBanAddRequest(reason, deleteMessagesDays)
}

package com.gitlab.kordlib.rest.builder.channel

import com.gitlab.kordlib.common.entity.ChannelType
import com.gitlab.kordlib.common.entity.Overwrite
import com.gitlab.kordlib.rest.builder.AuditRequestBuilder
import com.gitlab.kordlib.common.annotation.KordDsl
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.rest.json.request.GuildCreateChannelRequest
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

@KordDsl
class CategoryCreateBuilder : AuditRequestBuilder<GuildCreateChannelRequest> {
    override var reason: String? = null
    lateinit var name: String
    var position: Int? = null
    var nsfw: Boolean? = null
    val permissionOverwrites: MutableList<Overwrite> = mutableListOf()

    /**
     * adds a [Overwrite] for the [memberId].
     */
    @OptIn(ExperimentalContracts::class)
    inline fun addMemberOverwrite(memberId: Snowflake, builder: PermissionOverwriteBuilder.() -> Unit){
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }
        permissionOverwrites += PermissionOverwriteBuilder("member", memberId).apply(builder).toOverwrite()
    }

    /**
     * adds a [Overwrite] for the [roleId].
     */
    @OptIn(ExperimentalContracts::class)
    inline fun addRoleOverwrite(roleId: Snowflake, builder: PermissionOverwriteBuilder.() -> Unit){
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }
        permissionOverwrites += PermissionOverwriteBuilder("role", roleId).apply(builder).toOverwrite()
    }

    override fun toRequest(): GuildCreateChannelRequest = GuildCreateChannelRequest(
            name = name,
            position = position,
            nsfw = nsfw,
            permissionOverwrite = permissionOverwrites,
            type = ChannelType.GuildCategory
    )
}

package com.gitlab.kordlib.rest.builder.channel

import com.gitlab.kordlib.common.entity.Overwrite
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.rest.builder.AuditRequestBuilder
import com.gitlab.kordlib.common.annotation.KordDsl
import com.gitlab.kordlib.rest.json.request.ChannelModifyPatchRequest
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

@KordDsl
class CategoryModifyBuilder: AuditRequestBuilder<ChannelModifyPatchRequest> {
    override var reason: String? = null
    /**
     * The name of the category.
     */
    var name: String? = null

    /**
     * The position of this category in the guild's channel list.
     */
    var position: Int? = null

    /**
     *  The permission overwrites for this category.
     */
    var permissionOverwrites: MutableSet<Overwrite>? = null

    /**
     * adds a [Overwrite] for the [memberId].
     */
    @OptIn(ExperimentalContracts::class)
    inline fun addMemberOverwrite(memberId: Snowflake, builder: PermissionOverwriteBuilder.() -> Unit){
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }
        permissionOverwrites = (permissionOverwrites ?: mutableSetOf()).also { it.add(PermissionOverwriteBuilder("member", memberId).apply(builder).toOverwrite()) }
    }

    /**
     * adds a [Overwrite] for the [roleId].
     */
    @OptIn(ExperimentalContracts::class)
    inline fun addRoleOverwrite(roleId: Snowflake, builder: PermissionOverwriteBuilder.() -> Unit){
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }
        permissionOverwrites = (permissionOverwrites ?: mutableSetOf()).also { PermissionOverwriteBuilder("role", roleId).apply(builder).toOverwrite() }
    }

    override fun toRequest(): ChannelModifyPatchRequest = ChannelModifyPatchRequest(
            name = name,
            position = position,
            permissionOverwrites = permissionOverwrites?.toList()
    )
}

package com.gitlab.kordlib.rest.builder.channel

import com.gitlab.kordlib.common.entity.Permissions
import com.gitlab.kordlib.rest.builder.AuditRequestBuilder
import com.gitlab.kordlib.common.annotation.KordDsl
import com.gitlab.kordlib.rest.json.request.ChannelPermissionEditRequest

@KordDsl
class ChannelPermissionModifyBuilder(private var type: String) : AuditRequestBuilder<ChannelPermissionEditRequest> {

    override var reason: String? = null

    /**
     * The permissions that are explicitly allowed for this channel.
     */
    var allowed: Permissions = Permissions()

    /**
     * The permisssions that are explicitly denied for this channel.
     */
    var denied: Permissions = Permissions()

    override fun toRequest(): ChannelPermissionEditRequest = ChannelPermissionEditRequest(allowed, denied, type)

}

package com.gitlab.kordlib.rest.builder.channel

import com.gitlab.kordlib.common.entity.Overwrite
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.rest.builder.AuditRequestBuilder
import com.gitlab.kordlib.common.annotation.KordDsl
import com.gitlab.kordlib.rest.json.request.ChannelModifyPatchRequest

@KordDsl
class TextChannelModifyBuilder : AuditRequestBuilder<ChannelModifyPatchRequest> {
    override var reason: String? = null
    var name: String? = null
    var position: Int? = null
    var topic: String? = null
    var nsfw: Boolean? = null
    var parentId: Snowflake? = null
    var rateLimitPerUser: Int? = null
    var permissionOverwrites: Set<Overwrite>? = null

    override fun toRequest(): ChannelModifyPatchRequest = ChannelModifyPatchRequest(
            name = name,
            position = position,
            topic = topic,
            nsfw = nsfw,
            rateLimitPerUser = rateLimitPerUser,
            permissionOverwrites = permissionOverwrites?.toList(),
            parentId = parentId?.value
    )

}

@KordDsl
class VoiceChannelModifyBuilder : AuditRequestBuilder<ChannelModifyPatchRequest> {
    override var reason: String? = null
    var name: String? = null
    var position: Int? = null
    var parentId: Snowflake? = null
    var bitrate: Int? = null
    var userLimit: Int? = null
    var permissionOverwrites: Set<Overwrite>? = null

    override fun toRequest(): ChannelModifyPatchRequest = ChannelModifyPatchRequest(
            name = name,
            position = position,
            parentId = parentId?.value,
            bitrate = bitrate,
            userLimit = userLimit,
            permissionOverwrites = permissionOverwrites?.toList()
    )
}

@KordDsl
class NewsChannelModifyBuilder : AuditRequestBuilder<ChannelModifyPatchRequest> {
    override var reason: String? = null
    var name: String? = null
    var position: Int? = null
    var topic: String? = null
    var nsfw: Boolean? = null
    var parentId: Snowflake? = null
    var permissionOverwrites: Set<Overwrite>? = null

    override fun toRequest(): ChannelModifyPatchRequest = ChannelModifyPatchRequest(
            name = name,
            position = position,
            topic = topic,
            nsfw = nsfw,
            permissionOverwrites = permissionOverwrites?.toList(),
            parentId = parentId?.value
    )
}

@KordDsl
class StoreChannelModifyBuilder : AuditRequestBuilder<ChannelModifyPatchRequest> {
    override var reason: String? = null
    var name: String? = null
    var position: Int? = null
    val permissionOverwrites: MutableSet<Overwrite>? = null

    override fun toRequest(): ChannelModifyPatchRequest = ChannelModifyPatchRequest(
            name = name,
            position = position,
            permissionOverwrites = permissionOverwrites?.toList()
    )
}

package com.gitlab.kordlib.rest.builder.channel

import com.gitlab.kordlib.rest.builder.AuditRequestBuilder
import com.gitlab.kordlib.common.annotation.KordDsl
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.rest.json.request.GuildChannelPositionModifyRequest

@KordDsl
class GuildChannelPositionModifyBuilder: AuditRequestBuilder<GuildChannelPositionModifyRequest>  {
    override var reason: String? = null
    private val swaps: MutableList<Pair<String, Int>> = mutableListOf()

    fun move(pair: Pair<Snowflake, Int>) {
        swaps += pair.first.value to pair.second
    }

    fun move(vararg pairs: Pair<Snowflake, Int>) {
        swaps += pairs.map { it.first.value to it.second }
    }

    override fun toRequest(): GuildChannelPositionModifyRequest =
            GuildChannelPositionModifyRequest(swaps)
}

package com.gitlab.kordlib.rest.builder.channel

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.annotation.KordDsl
import com.gitlab.kordlib.common.entity.TargetUserType
import com.gitlab.kordlib.rest.builder.RequestBuilder
import com.gitlab.kordlib.rest.json.request.InviteCreateRequest

@KordDsl
class InviteCreateBuilder : RequestBuilder<InviteCreateRequest> {
    /**
     * The duration of invite in seconds before expiry, or 0 for never. 86400 (24 hours) by default.
     */
    var age: Int = 86400

    /**
     * The maximum number of uses, or 0 for unlimited. 0 by default.
     */
    var uses: Int = 0

    /**
     * 	Whether this invite only grants temporary membership. False by default.
     */
    var temporary: Boolean = false

    /**
     * Whether to reuse a similar invite (useful for creating many unique one time use invites). False by default.
     */
    var unique: Boolean = false

    /**
     * The target user id for this invite.
     */
    var targetUser: Snowflake? = null

    /**
     * The audit log reason for creating this invite.
     */
    var reason: String? = null

    override fun toRequest(): InviteCreateRequest = InviteCreateRequest(
            temporary = temporary,
            age = age,
            unique = unique,
            uses = uses,
            targetUser = targetUser?.value,
            targetUserType = targetUser?.let { TargetUserType.STREAM }
    )
}

package com.gitlab.kordlib.rest.builder.channel

import com.gitlab.kordlib.common.entity.Overwrite
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.rest.builder.AuditRequestBuilder
import com.gitlab.kordlib.common.annotation.KordDsl
import com.gitlab.kordlib.common.entity.ChannelType
import com.gitlab.kordlib.rest.json.request.GuildCreateChannelRequest

@KordDsl
class NewsChannelCreateBuilder: AuditRequestBuilder<GuildCreateChannelRequest> {
    override var reason: String? = null
    lateinit var name: String
    var topic: String? = null
    var nsfw: Boolean? = null
    var parentId: Snowflake? = null
    var position: Int? = null
    val permissionOverwrites: MutableList<Overwrite> = mutableListOf()

    override fun toRequest(): GuildCreateChannelRequest = GuildCreateChannelRequest(
            name = name,
            topic = topic,
            nsfw = nsfw,
            parentId = parentId?.value,
            position = position,
            permissionOverwrite = permissionOverwrites,
            type = ChannelType.GuildNews
    )
}

package com.gitlab.kordlib.rest.builder.channel

import com.gitlab.kordlib.common.entity.Overwrite
import com.gitlab.kordlib.common.entity.Permissions
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.annotation.KordDsl

@KordDsl
class PermissionOverwriteBuilder(private val type: String, private val id: Snowflake) {
    var allowed: Permissions = Permissions()
    var denied: Permissions = Permissions()

    fun toOverwrite(): Overwrite = Overwrite(id = id.value, allow = allowed.code, deny = denied.code, type = type)
}

package com.gitlab.kordlib.rest.builder.channel

import com.gitlab.kordlib.common.entity.ChannelType
import com.gitlab.kordlib.common.entity.Overwrite
import com.gitlab.kordlib.rest.builder.AuditRequestBuilder
import com.gitlab.kordlib.common.annotation.KordDsl
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.rest.json.request.GuildCreateChannelRequest

@KordDsl
class TextChannelCreateBuilder : AuditRequestBuilder<GuildCreateChannelRequest> {
    override var reason: String? = null
    lateinit var name: String
    var topic: String? = null
    var rateLimitPerUser: Int? = null
    var position: Int? = null
    var parentId: Snowflake? = null
    var nsfw: Boolean? = null
    val permissionOverwrites: MutableList<Overwrite> = mutableListOf()

    override fun toRequest(): GuildCreateChannelRequest = GuildCreateChannelRequest(
            name = name,
            topic = topic,
            rateLimitPerUser = rateLimitPerUser,
            position = position,
            parentId = parentId?.value,
            nsfw = nsfw,
            permissionOverwrite = permissionOverwrites,
            type = ChannelType.GuildText
    )
}

package com.gitlab.kordlib.rest.builder.channel

import com.gitlab.kordlib.common.entity.ChannelType
import com.gitlab.kordlib.common.entity.Overwrite
import com.gitlab.kordlib.rest.builder.AuditRequestBuilder
import com.gitlab.kordlib.common.annotation.KordDsl
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.rest.json.request.GuildCreateChannelRequest

@KordDsl
class VoiceChannelCreateBuilder : AuditRequestBuilder<GuildCreateChannelRequest> {
    override var reason: String? = null
    lateinit var name: String
    var bitrate: Int? = null
    var userLimit: Int? = null
    var parentId: Snowflake? = null
    var position: Int? = null
    val permissionOverwrites: MutableList<Overwrite> = mutableListOf()

    override fun toRequest(): GuildCreateChannelRequest = GuildCreateChannelRequest(
            name = name,
            bitrate = bitrate,
            userLimit = userLimit,
            parentId = parentId?.value,
            position = position,
            permissionOverwrite = permissionOverwrites,
            type = ChannelType.GuildVoice
    )
}

package com.gitlab.kordlib.rest.builder.guild

import com.gitlab.kordlib.common.annotation.KordDsl
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.rest.Image
import com.gitlab.kordlib.rest.builder.AuditRequestBuilder
import com.gitlab.kordlib.rest.json.request.EmojiCreateRequest
import com.gitlab.kordlib.rest.json.request.EmojiModifyRequest

@KordDsl
class EmojiCreateBuilder : AuditRequestBuilder<EmojiCreateRequest> {
    override var reason: String? = null
    lateinit var name: String
    lateinit var image: Image
    var roles: Set<Snowflake> = emptySet()

    override fun toRequest(): EmojiCreateRequest = EmojiCreateRequest(
            name = name,
            image = image.dataUri,
            roles = roles.map { it.value }
    )
}

package com.gitlab.kordlib.rest.builder.guild

import com.gitlab.kordlib.rest.builder.AuditRequestBuilder
import com.gitlab.kordlib.common.annotation.KordDsl
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.rest.json.request.EmojiModifyRequest

@KordDsl
class EmojiModifyBuilder : AuditRequestBuilder<EmojiModifyRequest> {
    override var reason: String? = null
    var name: String? = null
    var roles: Set<Snowflake>? = null

    override fun toRequest(): EmojiModifyRequest = EmojiModifyRequest(
            name = name,
            roles = roles?.map { it.value }
    )
}

package com.gitlab.kordlib.rest.builder.guild

import com.gitlab.kordlib.common.annotation.KordDsl
import com.gitlab.kordlib.common.entity.DefaultMessageNotificationLevel
import com.gitlab.kordlib.common.entity.ExplicitContentFilter
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.entity.VerificationLevel
import com.gitlab.kordlib.rest.builder.RequestBuilder
import com.gitlab.kordlib.rest.builder.channel.CategoryCreateBuilder
import com.gitlab.kordlib.rest.builder.channel.NewsChannelCreateBuilder
import com.gitlab.kordlib.rest.builder.channel.TextChannelCreateBuilder
import com.gitlab.kordlib.rest.builder.role.RoleCreateBuilder
import com.gitlab.kordlib.rest.json.request.GuildCreateChannelRequest
import com.gitlab.kordlib.rest.json.request.GuildCreateRequest
import com.gitlab.kordlib.rest.json.request.GuildRoleCreateRequest
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.random.Random

@KordDsl
class GuildCreateBuilder : RequestBuilder<GuildCreateRequest> {

    /**
     * Iterator that generates unique ids for roles and channels..
     */
    val snowflakeGenerator by lazy(LazyThreadSafetyMode.NONE) {
        generateSequence { Random.nextLong(0, Long.MAX_VALUE) }.filter {
            it !in roles.map { role -> role.id?.toLong() }
                    && it !in channels.map { channel -> channel.id?.toLong() }
                    && Snowflake(it) != systemChannelId
                    && Snowflake(it) != afkChannelId
        }.iterator()
    }

    /**
     * Generates a new unique [Snowflake] using the [snowflakeGenerator].
     */
    fun newUniqueSnowflake() = Snowflake(snowflakeGenerator.next())

    lateinit var name: String
    var region: String? = null
    var icon: String? = null
    var verificationLevel: VerificationLevel? = null
    var defaultMessageNotificationLevel: DefaultMessageNotificationLevel? = null
    var explicitContentFilter: ExplicitContentFilter? = null
    var everyoneRole: RoleCreateBuilder? = null
    val roles: MutableList<GuildRoleCreateRequest> = mutableListOf()
    val channels: MutableList<GuildCreateChannelRequest> = mutableListOf()

    /**
     * The id of the afk channel, this channel can be configured by supplying a channel with the same id.
     */
    val afkChannelId: Snowflake? = null

    /**
     * The afk timeout in seconds.
     */
    val afkTimeout: Int? = null

    /**
     * The id of the channel to which system messages are sent, this channel can be configured by supplying a channel with the same id.
     */
    val systemChannelId: Snowflake? = null

    @OptIn(ExperimentalContracts::class)
    inline fun textChannel(id: Snowflake = newUniqueSnowflake(), builder: TextChannelCreateBuilder.() -> Unit): Snowflake {
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }
        channels.add(TextChannelCreateBuilder().apply(builder).toRequest().copy(id = id.value))
        return id
    }

    @OptIn(ExperimentalContracts::class)
    inline fun newsChannel(id: Snowflake = newUniqueSnowflake(), builder: NewsChannelCreateBuilder.() -> Unit): Snowflake {
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }
        channels.add(NewsChannelCreateBuilder().apply(builder).toRequest().copy(id = id.value))
        return id
    }

    @OptIn(ExperimentalContracts::class)
    inline fun category(id: Snowflake = newUniqueSnowflake(), builder: CategoryCreateBuilder.() -> Unit): Snowflake {
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }
        channels.add(CategoryCreateBuilder().apply(builder).toRequest().copy(id = id.value))
        return id
    }

    @OptIn(ExperimentalContracts::class)
    inline fun role(id: Snowflake = newUniqueSnowflake(), builder: RoleCreateBuilder.() -> Unit): Snowflake {
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }
        roles += RoleCreateBuilder().apply(builder).toRequest().copy(id = id.value)
        return id
    }

    @OptIn(ExperimentalContracts::class)
    inline fun everyoneRole(builder: RoleCreateBuilder.() -> Unit) {
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }
        everyoneRole = RoleCreateBuilder().apply(builder)
    }

    override fun toRequest(): GuildCreateRequest = GuildCreateRequest(
            name,
            region,
            icon,
            verificationLevel,
            defaultMessageNotificationLevel,
            explicitContentFilter,
            if (roles.isEmpty()) null else everyoneRole?.let { roles + it.toRequest() } ?: roles,
            if (channels.isEmpty()) null else channels
    )
}

package com.gitlab.kordlib.rest.builder.guild

import com.gitlab.kordlib.common.entity.DefaultMessageNotificationLevel
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.entity.VerificationLevel
import com.gitlab.kordlib.rest.Image
import com.gitlab.kordlib.rest.builder.AuditRequestBuilder
import com.gitlab.kordlib.rest.json.request.GuildModifyRequest
import com.gitlab.kordlib.common.annotation.KordDsl
import java.util.*

@KordDsl
class GuildModifyBuilder : AuditRequestBuilder<GuildModifyRequest> {
    override var reason: String? = null
    var name: String? = null
    var region: Snowflake? = null
    var verificationLevel: VerificationLevel? = null
    var notificationLevel: DefaultMessageNotificationLevel? = null
    var afkChannelId: Snowflake? = null
    var afkTimeout: Int? = null
    var icon: Image? = null
    var ownerId: Snowflake? = null
    var splash: Image? = null

    /**
     * The id of the channel where "PUBLIC" guilds display rules and/or guidelines.
     */
    var rulesChannelId: Snowflake? = null

    /**
     * The id of the channel where admins and moderators of "PUBLIC" guilds receive notices from Discord.
     */
    var publicUpdatesChannelId: Snowflake? = null

    /**
     * The preferred locale of a "PUBLIC" guild used in server discovery and notices from Discord; defaults to "en-US".
     */
    var preferredLocale: Locale? = null

    override fun toRequest(): GuildModifyRequest = GuildModifyRequest(
            name = name,
            region = region?.value,
            verificationLevel = verificationLevel,
            defaultMessageNotificationLevel = notificationLevel,
            afkChannel = afkChannelId?.value,
            afkTimeout = afkTimeout,
            icon = icon?.dataUri,
            ownerId = ownerId?.value,
            spalsh = splash?.dataUri,
            preferredLocale = preferredLocale?.let { listOf(it.language, it.country).joinToString("-") },
            publicUpdatesChannelId = publicUpdatesChannelId?.value
    )
}

package com.gitlab.kordlib.rest.builder.integration

import com.gitlab.kordlib.rest.builder.RequestBuilder
import com.gitlab.kordlib.rest.json.request.GuildIntegrationModifyRequest
import com.gitlab.kordlib.rest.json.response.IntegrationExpireBehavior

/**
 * Builder for [modifying an integration](https://discord.com/developers/docs/resources/guild#modify-guild-integration).
 */
class IntegrationModifyBuilder : RequestBuilder<GuildIntegrationModifyRequest> {

    /**
     * the behavior when an integration subscription lapses.
     */
    var expireBehavior: IntegrationExpireBehavior? = null

    /**
     * 	Period in days where the integration will ignore lapsed subscriptions
     */
    var expirePeriodInDays: Int? = null

    /**
     * whether emoticons should be synced for this integration (twitch only currently).
     */
    var enableEmoticons: Boolean? = null

    override fun toRequest(): GuildIntegrationModifyRequest = GuildIntegrationModifyRequest(
            expireBehavior?.code, expirePeriodInDays, enableEmoticons
    )

}

package com.gitlab.kordlib.rest.builder.member

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.annotation.KordDsl
import com.gitlab.kordlib.rest.builder.RequestBuilder
import com.gitlab.kordlib.rest.json.request.GuildMemberAddRequest

@KordDsl
class MemberAddBuilder : RequestBuilder<GuildMemberAddRequest> {
    lateinit var token: String
    var nickname: String? = null
    val roles: Set<Snowflake> = mutableSetOf()
    var muted: Boolean? = null
    var deafened: Boolean? = null

    override fun toRequest(): GuildMemberAddRequest =
            GuildMemberAddRequest(token, nickname, roles.map { it.value }, mute = muted, deaf = deafened)

}

package com.gitlab.kordlib.rest.builder.member

import com.gitlab.kordlib.rest.builder.AuditRequestBuilder
import com.gitlab.kordlib.common.annotation.KordDsl
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.rest.json.request.GuildMemberModifyRequest

@KordDsl
class MemberModifyBuilder : AuditRequestBuilder<GuildMemberModifyRequest> {
    override var reason: String? = null
    var voiceChannelId: Snowflake? = null
    var muted: Boolean? = null
    var deafened: Boolean? = null
    var nickname: String? = null
    var roles: Set<Snowflake>? = null

    override fun toRequest(): GuildMemberModifyRequest = GuildMemberModifyRequest(
            nick = nickname,
            channelId = voiceChannelId?.value,
            mute = muted,
            deaf = deafened,
            roles = roles?.map { it.value }
    )
}

package com.gitlab.kordlib.rest.builder.message

import com.gitlab.kordlib.common.annotation.KordDsl
import com.gitlab.kordlib.rest.builder.RequestBuilder
import com.gitlab.kordlib.rest.json.request.*
import java.awt.Color
import java.time.Instant
import java.time.format.DateTimeFormatter

/**
 * A builder for discord embeds.
 *
 * Inline Markdown links are supported in in all description-like fields.
 */
@KordDsl
@Suppress("unused")
class EmbedBuilder : RequestBuilder<EmbedRequest> {
    companion object {
        /**
         * Utility default value for fields.
         * While blank values are not allowed, this value can be used to trick Discord into rendering a Field as empty.
         */
        const val ZERO_WIDTH_SPACE = "\u200E"
    }

    /**
     * The title of the embed. Limited to the length of [Limits.title].
     */
    var title: String? = null

    /**
     * The description of the embed. Limited to the length of [Limits.description].
     */
    var description: String? = null

    /**
     * The url of the embed's [title].
     */
    var url: String? = null

    /**
     * The timestamp displayed at the bottom of the embed.
     */
    var timestamp: Instant? = null

    /**
     * The color of the embed.
     */
    var color: Color? = null

    /**
     * The image url of the embed.
     */
    var image: String? = null

    /**
     * The footer of the embed.
     */
    var footer: Footer? = null

    /**
     * The thumbnail of the embed.
     */
    var thumbnail: Thumbnail? = null

    /**
     * The author of the embed.
     */
    var author: Author? = null

    /**
     * The embed fields.
     */
    val fields: MutableList<Field> = mutableListOf()

    /**
     * Adds or updates the [footer] as configured by the [builder].
     */
    inline fun footer(builder: Footer.() -> Unit) {
        footer = (footer ?: Footer()).apply(builder)
    }

    /**
     * Adds or updates the [thumbnail] as configured by the [builder].
     */
    inline fun thumbnail(builder: Thumbnail.() -> Unit) {
        thumbnail = (thumbnail ?: Thumbnail()).apply(builder)
    }

    /**
     * Adds or updates the [author] as configured by the [builder].
     */
    inline fun author(builder: Author.() -> Unit) {
        author = (author ?: Author()).apply(builder)
    }

    /**
     * Adds a new [Field] configured by the [builder].
     */
    inline fun field(builder: Field.() -> Unit) {
        fields += Field().apply(builder)
    }

    /**
     * Adds a new [Field] using the given [name] and [value].
     *
     * @param inline Whether the field should be rendered inline, `false` by default.
     *
     * @param value The value or 'description' of the [Field], [ZERO_WIDTH_SPACE] by default.
     * Limited to the length of [Field.Limits.value].
     *
     * @param name The name or 'title' of the [Field], [ZERO_WIDTH_SPACE] by default.
     * Limited in to the length of [Field.Limits.name].
     *
     */
    inline fun field(name: String, inline: Boolean = false, value: () -> String = { ZERO_WIDTH_SPACE }) {
        val field = Field()
        field.name = name
        field.inline = inline
        field.value = value()

        fields += field
    }

    override fun toRequest(): EmbedRequest = EmbedRequest(
            title,
            "embed",
            description,
            url,
            timestamp?.let { DateTimeFormatter.ISO_INSTANT.format(it) },
            color?.rgb?.and(0xFFFFFF),
            footer?.toRequest(),
            image?.let(::EmbedImageRequest),
            thumbnail?.toRequest(),
            author?.toRequest(),
            fields.map { it.toRequest() }
    )

    @KordDsl
    class Thumbnail : RequestBuilder<EmbedThumbnailRequest> {

        /**
         * The image url of the thumbnail. This field is required.
         */
        lateinit var url: String

        override fun toRequest() = EmbedThumbnailRequest(url)
    }

    @KordDsl
    class Footer : RequestBuilder<EmbedFooterRequest> {

        /**
         * The text of the footer. This field is required and limited to the length of [Limits.text].
         */
        lateinit var text: String

        /**
         * The icon url to display.
         */
        var icon: String? = null

        override fun toRequest() = EmbedFooterRequest(text, icon)

        object Limits {
            const val text = 2048
        }
    }

    @KordDsl
    class Author : RequestBuilder<EmbedAuthorRequest> {

        /**
         * The name of the author. This field is required if [url] is not null.
         */
        var name: String? = null

        /**
         * The link that will be applied to the author's [name]. [name] is a mandatory field if [url] is not null.
         */
        var url: String? = null

        /**
         * The image url that will be displayed next to the author's name.
         */
        var icon: String? = null

        override fun toRequest() = EmbedAuthorRequest(name, url, icon)

        object Limits {
            /**
             * The maximum length of the [Author.name] field.
             */
            const val name = 256
        }
    }

    @KordDsl
    class Field : RequestBuilder<EmbedFieldRequest> {

        /**
         *  The value or 'description' of the [Field], [ZERO_WIDTH_SPACE] by default.
         *  Limited to the length of [Limits.value].
         *
         *  Blank values are not allowed, resulting an exception being thrown on the completion of the request.
         *  Use [ZERO_WIDTH_SPACE] instead to simulate an empty [value].
         */
        @Suppress("JoinDeclarationAndAssignment", "UNNECESSARY_LATEINIT")
        lateinit var value: String

        /**
         * The name or 'title' of the [Field], [ZERO_WIDTH_SPACE] by default.
         * Limited in to the length of [Limits.name].
         *
         * Blank values are not allowed, resulting an exception being thrown on the completion of the request.
         * Use [ZERO_WIDTH_SPACE] instead to simulate an empty [value].
         */
        @Suppress("JoinDeclarationAndAssignment", "UNNECESSARY_LATEINIT")
        lateinit var name: String

        /**
         * Whether the field should be rendered inline, `false` by default.
         */
        var inline: Boolean = false

        init {
            //these declarations and assignments are separated to maintain binary compatibility.
            //`lateinit` fields expose the JVM fields, and this was the cleanest solution I could think of
            // to maintain that field access.
            value = ZERO_WIDTH_SPACE
            name = ZERO_WIDTH_SPACE
        }

        override fun toRequest() = EmbedFieldRequest(name, value, inline)

        object Limits {
            /**
             * The maximum length of the [Field.name] field.
             */
            const val name = 256

            /**
             * The maximum length of the [Field.value] field.
             */
            const val value = 2048
        }
    }

    object Limits {
        /**
         * The maximum length of the [EmbedBuilder.title] field.
         */
        const val title = 256

        /**
         * The maximum length of the [EmbedBuilder.description] field.
         */
        const val description = 2048

        /**
         * The maximum amount of [EmbedBuilder.Field] in an [EmbedBuilder].
         */
        const val fieldCount = 25

        /**
         * The maximum length of all text inside the [EmbedBuilder].
         */
        const val total = 6000
    }
}

package com.gitlab.kordlib.rest.builder.message

import com.gitlab.kordlib.common.annotation.KordDsl
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.rest.builder.RequestBuilder
import com.gitlab.kordlib.rest.json.request.AllowedMentions
import com.gitlab.kordlib.rest.json.request.MessageCreateRequest
import com.gitlab.kordlib.rest.json.request.MultipartMessageCreateRequest
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.InputStream
import java.nio.file.Files
import java.nio.file.Path

@KordDsl
class MessageCreateBuilder : RequestBuilder<MultipartMessageCreateRequest> {
    var content: String? = null
    var nonce: String? = null
    var tts: Boolean? = null
    var embed: EmbedBuilder? = null
    var allowedMentions: AllowedMentionsBuilder? = null
    val files: MutableList<Pair<String, InputStream>> = mutableListOf()

    inline fun embed(block: EmbedBuilder.() -> Unit) {
        embed = (embed ?: EmbedBuilder()).apply(block)
    }

    fun addFile(name: String, content: InputStream) {
        files += name to content
    }

    suspend fun addFile(path: Path) = withContext(Dispatchers.IO) {
        addFile(path.fileName.toString(), Files.newInputStream(path))
    }

    /**
     * Configures the mentions that should trigger a ping. Not calling this function will result in the default behavior
     * (ping everything), calling this function but not configuring it before the request is build will result in all
     * pings being ignored.
     */
    inline fun allowedMentions(block: AllowedMentionsBuilder.() -> Unit = {}) {
        allowedMentions = (allowedMentions ?: AllowedMentionsBuilder()).apply(block)
    }

    override fun toRequest(): MultipartMessageCreateRequest = MultipartMessageCreateRequest(
            MessageCreateRequest(content, nonce, tts, embed?.toRequest(), allowedMentions?.build()),
            files
    )

}

/**
 * The mentions that should trigger a ping. See the [Discord documentation](https://discord.com/developers/docs/resources/channel#allowed-mentions-object).
 *
 */
class AllowedMentionsBuilder {
    /**
     * The roles that should be mentioned in this message, any id that is mentioned in this list but not present in the
     * [MessageCreateBuilder] will be ignored.
     */
    val roles: MutableSet<Snowflake> = mutableSetOf()

    /**
     * The users that should be mentioned in this message, any id that is mentioned in this list but not present in the
     * [MessageCreateBuilder] will be ignored.
     */
    val users: MutableSet<Snowflake> = mutableSetOf()

    /**
     * The types of pings that should trigger in this message. Selecting [MentionTypes.Users] or [MentionTypes.Roles]
     * together with any value in [users] or [roles] respectively will result in an error.
     */
    val types: MutableSet<MentionTypes> = mutableSetOf()

    /**
     * Adds the type to the list of types that should receive a ping.
     */
    operator fun MentionTypes.unaryPlus() = types.add(this)

    fun build(): AllowedMentions = AllowedMentions(
            parse = types.map { it.serialName },
            users = users.map { it.value },
            roles = roles.map { it.value }
    )

}

enum class MentionTypes(val serialName: String) {
    Roles("roles"), Users("users"), Everyone("everyone")
}

package com.gitlab.kordlib.rest.builder.message

import com.gitlab.kordlib.common.entity.Flags
import com.gitlab.kordlib.common.annotation.KordDsl
import com.gitlab.kordlib.rest.builder.RequestBuilder
import com.gitlab.kordlib.rest.json.request.MessageEditPatchRequest
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

@KordDsl
class MessageModifyBuilder : RequestBuilder<MessageEditPatchRequest> {
    var content: String? = null
    var embed: EmbedBuilder? = null
    var flags: Flags? = null
    var allowedMentions: AllowedMentionsBuilder? = null

    @OptIn(ExperimentalContracts::class)
    inline fun embed(block: EmbedBuilder.() -> Unit) {
        contract {
            callsInPlace(block, InvocationKind.EXACTLY_ONCE)
        }
        embed = (embed ?: EmbedBuilder()).also(block)
    }

    /**
     * Configures the mentions that should trigger a ping. Not calling this function will result in the default behavior
     * (ping everything), calling this function but not configuring it before the request is build will result in all
     * pings being ignored.
     */
    @OptIn(ExperimentalContracts::class)
    inline fun allowedMentions(block: AllowedMentionsBuilder.() -> Unit = {}) {
        contract {
            callsInPlace(block, InvocationKind.EXACTLY_ONCE)
        }
        allowedMentions = (allowedMentions ?: AllowedMentionsBuilder()).apply(block)
    }

    override fun toRequest(): MessageEditPatchRequest = MessageEditPatchRequest(content, embed?.toRequest(), flags, allowedMentions?.build())
}

package com.gitlab.kordlib.rest.builder.role

import com.gitlab.kordlib.common.entity.Permissions
import com.gitlab.kordlib.rest.builder.AuditRequestBuilder
import com.gitlab.kordlib.common.annotation.KordDsl
import com.gitlab.kordlib.rest.json.request.GuildRoleCreateRequest
import java.awt.Color

@KordDsl
class RoleCreateBuilder : AuditRequestBuilder<GuildRoleCreateRequest>{
    override var reason: String? = null
    var color: Color? = null
    var hoist: Boolean = false
    var name: String? = null
    var mentionable: Boolean = false
    var permissions: Permissions? = null

    override fun toRequest(): GuildRoleCreateRequest = GuildRoleCreateRequest(
            color = color?.rgb?.and(0xFFFFFF) ?: 0,
            separate = hoist,
            name = name,
            mentionable = mentionable,
            permissions = permissions
    )
}

package com.gitlab.kordlib.rest.builder.role

import com.gitlab.kordlib.common.entity.Permissions
import com.gitlab.kordlib.rest.builder.AuditRequestBuilder
import com.gitlab.kordlib.common.annotation.KordDsl
import com.gitlab.kordlib.rest.json.request.GuildRoleModifyRequest
import java.awt.Color

@KordDsl
class RoleModifyBuilder : AuditRequestBuilder<GuildRoleModifyRequest> {
    override var reason: String? = null
    var name: String? = null
    var color: Color? = null
    var hoist: Boolean? = null
    var mentionable: Boolean? = null
    var permissions: Permissions? = null

    override fun toRequest(): GuildRoleModifyRequest = GuildRoleModifyRequest(
            name = name,
            color = color?.rgb?.and(0xFFFFFF),
            separate = hoist,
            mentionable = mentionable,
            permissions = permissions
    )
}

package com.gitlab.kordlib.rest.builder.role

import com.gitlab.kordlib.rest.builder.AuditRequestBuilder
import com.gitlab.kordlib.common.annotation.KordDsl
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.rest.json.request.GuildRolePositionModifyRequest

@KordDsl
class RolePositionsModifyBuilder : AuditRequestBuilder<GuildRolePositionModifyRequest> {
    override var reason: String? = null
    private val swaps: MutableList<Pair<String, Int>> = mutableListOf()

    fun move(pair: Pair<Snowflake, Int>) {
        swaps += pair.first.value to pair.second
    }

    fun move(vararg pairs: Pair<Snowflake, Int>) {
        swaps += pairs.map { it.first.value to it.second }
    }

    override fun toRequest(): GuildRolePositionModifyRequest =
            GuildRolePositionModifyRequest(swaps)
}

package com.gitlab.kordlib.rest.builder.user

import com.gitlab.kordlib.rest.Image
import com.gitlab.kordlib.common.annotation.KordDsl
import com.gitlab.kordlib.rest.builder.RequestBuilder
import com.gitlab.kordlib.rest.json.request.CurrentUserModifyRequest

@KordDsl
class CurrentUserModifyBuilder : RequestBuilder<CurrentUserModifyRequest> {

    var username: String? = null
    var avatar: Image? = null

    override fun toRequest(): CurrentUserModifyRequest = CurrentUserModifyRequest(username, avatar?.dataUri)

}

package com.gitlab.kordlib.rest.builder.user

import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.common.annotation.KordDsl
import com.gitlab.kordlib.rest.builder.RequestBuilder
import com.gitlab.kordlib.rest.json.request.GroupDMCreateRequest

@KordDsl
class GroupDMCreateBuilder : RequestBuilder<GroupDMCreateRequest> {

    val tokens: MutableList<String> = mutableListOf()
    val nicknames: MutableMap<Snowflake, String> = mutableMapOf()

    override fun toRequest(): GroupDMCreateRequest = GroupDMCreateRequest(
            tokens.toList(),
            nicknames.mapKeys { it.value }
    )

}

package com.gitlab.kordlib.rest.builder.webhook

import com.gitlab.kordlib.common.annotation.KordDsl
import com.gitlab.kordlib.rest.builder.RequestBuilder
import com.gitlab.kordlib.rest.builder.message.EmbedBuilder
import com.gitlab.kordlib.rest.json.request.EmbedRequest
import com.gitlab.kordlib.rest.json.request.MultiPartWebhookExecuteRequest
import com.gitlab.kordlib.rest.json.request.WebhookExecuteRequest
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.nio.file.Files
import java.nio.file.Path
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

@KordDsl
class ExecuteWebhookBuilder: RequestBuilder<MultiPartWebhookExecuteRequest> {
    var content: String? = null
    var username: String? = null
    var avatarUrl: String? = null
    var tts: Boolean? = null
    private var file: Pair<String, java.io.InputStream>? = null
    val embeds: MutableList<EmbedRequest> = mutableListOf()

    fun setFile(name: String, content: java.io.InputStream) {
        file = name to content
    }

    suspend fun setFile(path: Path) = withContext(Dispatchers.IO) {
        setFile(path.fileName.toString(), Files.newInputStream(path))
    }

    @OptIn(ExperimentalContracts::class)
    inline fun embed(builder: EmbedBuilder.() -> Unit) {
        contract {
            callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
        }
        embeds += EmbedBuilder().apply(builder).toRequest()
    }

    override fun toRequest() : MultiPartWebhookExecuteRequest = MultiPartWebhookExecuteRequest(
        WebhookExecuteRequest(content, username, avatarUrl, tts, embeds), file
    )
}

package com.gitlab.kordlib.rest.builder.webhook

import com.gitlab.kordlib.rest.builder.AuditRequestBuilder
import com.gitlab.kordlib.common.annotation.KordDsl
import com.gitlab.kordlib.rest.json.request.WebhookCreateRequest
import com.gitlab.kordlib.rest.Image

@KordDsl
class WebhookCreateBuilder: AuditRequestBuilder<WebhookCreateRequest> {
    override var reason: String? = null
    lateinit var name: String
    var avatar: Image? = null

    override fun toRequest() = WebhookCreateRequest(name, avatar?.dataUri)
}

package com.gitlab.kordlib.rest.builder.webhook

import com.gitlab.kordlib.rest.builder.AuditRequestBuilder
import com.gitlab.kordlib.common.annotation.KordDsl
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.rest.Image
import com.gitlab.kordlib.rest.json.request.WebhookModifyRequest

@KordDsl
class WebhookModifyBuilder: AuditRequestBuilder<WebhookModifyRequest> {
    override var reason: String? = null
    var name: String? = null
    var avatar: Image? = null
    var channelId: Snowflake? = null

    override fun toRequest(): WebhookModifyRequest = WebhookModifyRequest(
            name = name,
            avatar = avatar?.dataUri,
            channelId = channelId?.value
    )
}

package com.gitlab.kordlib.rest.json.request

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
class ChannelFollowRequest(
        @SerialName("webhook_channel_id")
        val webhookChannelId: String
)

package com.gitlab.kordlib.rest.json.request

import com.gitlab.kordlib.common.entity.Overwrite
import com.gitlab.kordlib.common.entity.Permissions
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class ChannelModifyPutRequest(
        val name: String,
        val position: Int,
        val topic: String? = null,
        val nsfw: Boolean? = null,
        @SerialName("rate_limit_per_user")
        val rateLimitPerUser: Int? = null,
        val bitrate: Int? = null,
        @SerialName("user_limit")
        val userLimit: Int? = null,
        @SerialName("permission_overwrites")
        val permissionOverwrites: List<Overwrite>,
        @SerialName("parent_id")
        val parentId: String? = null
)

@Serializable
data class ChannelModifyPatchRequest(
        val name: String? = null,
        val position: Int? = null,
        val topic: String? = null,
        val nsfw: Boolean? = null,
        @SerialName("rate_limit_per_user")
        val rateLimitPerUser: Int? = null,
        val bitrate: Int? = null,
        @SerialName("user_limit")
        val userLimit: Int? = null,
        @SerialName("permission_overwrites")
        val permissionOverwrites: List<Overwrite>? = null,
        @SerialName("parent_id")
        val parentId: String? = null
)

@Serializable
data class ChannelPermissionEditRequest(val allow: Permissions, val deny: Permissions, val type: String)

package com.gitlab.kordlib.rest.json.request

import kotlinx.serialization.Serializable

@Serializable
data class EmojiCreateRequest(
        val name: String,
        val image: String,
        val roles: List<String>
)

@Serializable
data class EmojiModifyRequest(
        val name: String? = null,
        val roles: List<String>? = null
)

package com.gitlab.kordlib.rest.json.request

import com.gitlab.kordlib.common.entity.*
import kotlinx.serialization.*
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.StructureKind
import kotlinx.serialization.descriptors.buildSerialDescriptor
import kotlinx.serialization.encoding.Encoder

@Serializable
data class GuildCreateRequest(
        val name: String,
        val region: String? = null,
        val icon: String? = null,
        @SerialName("verification_level")
        val verificationLevel: VerificationLevel? = null,
        @SerialName("default_message_notifications")
        val defaultNotificationLevel: DefaultMessageNotificationLevel? = null,
        val explicitContentFilter: ExplicitContentFilter? = null,
        val roles: List<GuildRoleCreateRequest>? = null,
        val channels: List<GuildCreateChannelRequest>? = null,
        @SerialName("afk_channel_id")
        val afkChannelId: String? = null,
        @SerialName("afk_timeout")
        val afkTimeout: Int? = null,
        @SerialName("system_channel_id")
        val systemChannelId: String? = null
)

@Serializable
data class GuildCreateChannelRequest(
        val name: String,
        val type: ChannelType? = null,
        val topic: String? = null,
        val bitrate: Int? = null,
        @SerialName("user_limit")
        val userLimit: Int? = null,
        @SerialName("rate_limit_per_user")
        val rateLimitPerUser: Int? = null,
        val position: Int? = null,
        @SerialName("permission_overwrites")
        val permissionOverwrite: List<Overwrite>? = null,
        @SerialName("parent_id")
        val parentId: String? = null,
        val nsfw: Boolean? = null,
        val id: String? = null
)

data class GuildChannelPositionModifyRequest(val swaps: List<Pair<String, Int>>) {

    companion object Serializer : SerializationStrategy<GuildChannelPositionModifyRequest> {
        @OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class)
        override val descriptor: SerialDescriptor
            get() = buildSerialDescriptor("GuildChannelPosition", StructureKind.LIST)

        override fun serialize(encoder: Encoder, value: GuildChannelPositionModifyRequest) {
            val positions = value.swaps.map { ChannelPosition(it.first, it.second) }
            ListSerializer(ChannelPosition.serializer()).serialize(encoder, positions)
        }

    }

    @Serializable
    private data class ChannelPosition(val id: String, val position: Int)
}

@Serializable
data class GuildMemberAddRequest(
        @SerialName("access_token") val token: String,
        val nick: String? = null,
        val roles: List<String>? = null,
        val mute: Boolean? = null,
        val deaf: Boolean? = null
)

@Serializable
data class GuildMemberModifyRequest(
        val nick: String? = null,
        val roles: List<String>? = null,
        val mute: Boolean? = null,
        val deaf: Boolean? = null,
        @SerialName("channel_id")
        val channelId: String? = null
)

@Serializable
data class GuildBanAddRequest(
        val reason: String? = null,
        @SerialName("delete-message-days")
        val deleteMessagesDays: Int? = null
)

@Serializable
data class GuildRoleCreateRequest(
        val name: String? = null,
        val permissions: Permissions? = null,
        val color: Int = 0,
        @SerialName("hoist")
        val separate: Boolean = false,
        val mentionable: Boolean = false,
        val id: String? = null
)

data class GuildRolePositionModifyRequest(val swaps: List<Pair<String, Int>>) {

    companion object Serializer : SerializationStrategy<GuildRolePositionModifyRequest> {
        @OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
        override val descriptor: SerialDescriptor
            get() = buildSerialDescriptor("GuildRolePosition", StructureKind.LIST)

        override fun serialize(encoder: Encoder, obj: GuildRolePositionModifyRequest) {
            val positions = obj.swaps.map { RolePosition(it.first, it.second) }
            ListSerializer(RolePosition.serializer()).serialize(encoder, positions)
        }

    }

    @Serializable
    private data class RolePosition(val id: String, val position: Int)
}

@Serializable
data class GuildRoleModifyRequest(
        val name: String? = null,
        val permissions: Permissions? = null,
        val color: Int? = null,
        @SerialName("hoist")
        val separate: Boolean? = null,
        val mentionable: Boolean? = null
)

@Serializable
data class GuildIntegrationCreateRequest(val type: Int, val id: String)

@Serializable
data class GuildIntegrationModifyRequest(
        @SerialName("expire_behavior")
        val expireBehavior: Int? = null,
        @SerialName("expire_grace_period")
        val expirePeriod: Int? = null,
        @SerialName("enable_emoticons")
        val emoticons: Boolean? = null
)

@Serializable
data class GuildEmbedModifyRequest(
        val enabled: Boolean,
        @SerialName("channel_id")
        val channelId: String
)

@Serializable
data class CurrentUserNicknameModifyRequest(val nick: String? = null)

@Serializable
data class GuildModifyRequest(
        val name: String? = null,
        val region: String? = null,
        @SerialName("verification_level")
        val verificationLevel: VerificationLevel? = null,
        @SerialName("default_message_notifications")
        val defaultMessageNotificationLevel: DefaultMessageNotificationLevel? = null,
        @SerialName("explicit_content_filter")
        val contentFilter: ExplicitContentFilter? = null,
        @SerialName("afk_channel_id")
        val afkChannel: String? = null,
        @SerialName("afk_timeout")
        val afkTimeout: Int? = null,
        val icon: String? = null,
        @SerialName("owner_id")
        val ownerId: String? = null,
        val spalsh: String? = null,
        @SerialName("system_channel_id")
        val systemChannelId: String? = null,
        val banner: String? = null,
        @SerialName("preferred_locale")
        val preferredLocale: String? = null,
        @SerialName("public_updates_channel_id")
        val publicUpdatesChannelId: String? = null
)

package com.gitlab.kordlib.rest.json.request

import com.gitlab.kordlib.common.entity.TargetUserType
import kotlinx.serialization.*

@Serializable
data class InviteCreateRequest(
        @SerialName("max_age")
        val age: Int? = null,
        @SerialName("max_uses")
        val uses: Int? = null,
        val temporary: Boolean? = null,
        val unique: Boolean? = null,
        @SerialName("target_user")
        val targetUser: String? = null,
        @SerialName("target_user_type")
        val targetUserType: TargetUserType? = null
)

package com.gitlab.kordlib.rest.json.request

import com.gitlab.kordlib.common.entity.Flags
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class MessageCreateRequest(
        val content: String? = null,
        val nonce: String? = null,
        val tts: Boolean? = null,
        val embed: EmbedRequest? = null,
        @SerialName("allowed_mentions")
        val allowedMentions: AllowedMentions? = null
)

@Serializable
data class AllowedMentions(val parse: List<String>, val users: List<String>, val roles: List<String>)

data class MultipartMessageCreateRequest(
        val request: MessageCreateRequest,
        val files: List<Pair<String, java.io.InputStream>> = emptyList()
)

@Serializable
data class EmbedRequest(
        val title: String?,
        val type: String?,
        val description: String?,
        val url: String?,
        val timestamp: String? = null,
        val color: Int? = null,
        val footer: EmbedFooterRequest? = null,
        val image: EmbedImageRequest? = null,
        val thumbnail: EmbedThumbnailRequest? = null,
        val author: EmbedAuthorRequest? = null,
        val fields: List<EmbedFieldRequest>? = null
)

@Serializable
data class EmbedFooterRequest(
        val text: String,
        @SerialName("icon_url")
        val iconUrl: String? = null
)

@Serializable
data class EmbedImageRequest(val url: String)

@Serializable
data class EmbedThumbnailRequest(val url: String)

@Serializable
data class EmbedAuthorRequest(
        val name: String? = null,
        val url: String? = null,
        @SerialName("icon_url")
        val iconUrl: String? = null
)

@Serializable
data class EmbedFieldRequest(
        val name: String,
        val value: String,
        val inline: Boolean? = null
)

@Serializable
data class MessageEditPatchRequest(
        val content: String? = null,
        val embed: EmbedRequest? = null,
        val flags: Flags? = null,
        @SerialName("allowed_mentions")
        val allowedMentions: AllowedMentions? = null
)

@Serializable
data class BulkDeleteRequest(val messages: List<String>)

package com.gitlab.kordlib.rest.json.request

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class DMCreateRequest(
        @SerialName("recipient_id")
        val userId: String
)

@Serializable
data class GroupDMCreateRequest(
        @SerialName("access_tokens")
        val tokens: List<String>,
        val nick: Map<String, String>)

@Serializable
data class CurrentUserModifyRequest(
        val username: String? = null,
        val avatar: String? = null
)

@Serializable
data class UserAddDMRequest(
        @SerialName("access_token")
        val token: String,
        val nick: String
)

package com.gitlab.kordlib.rest.json.request

import kotlinx.serialization.SerialName
import kotlinx. serialization.Serializable

@Serializable
data class WebhookCreateRequest(val name: String, val avatar: String?)

@Serializable
data class WebhookModifyRequest(
        val name: String? = null,
        val avatar: String? = null,
        @SerialName("channel_id")
        val channelId: String? = null
)

@Serializable
data class WebhookExecuteRequest(
        val content: String?,
        val username: String? = null,
        @SerialName("avatar_url")
        val avatar: String? = null,
        val tts: Boolean? = null,
        val embeds: List<EmbedRequest>? = null
)

data class MultiPartWebhookExecuteRequest(
        val request: WebhookExecuteRequest,
        val file: Pair<String, java.io.InputStream>?
)

@file:Suppress("ArrayInDataClass")

package com.gitlab.kordlib.rest.json.response

import com.gitlab.kordlib.common.entity.DiscordTeam
import com.gitlab.kordlib.common.entity.DiscordUser
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

/**
 * Payload gotten from [GET /oauth2/applications/@me](https://discord.com/developers/docs/topics/oauth2#get-current-application-information)
 */
@Serializable
data class ApplicationInfoResponse(
        val id: String,
        val name: String,
        val icon: String? = null,
        val description: String? = null,
        @SerialName("rpc_origins")
        val rpcOrigins: Array<String>? = null,
        @SerialName("bot_public")
        val botPublic: Boolean,
        @SerialName("bot_require_code_grant")
        val botRequireCodeGrant: Boolean,
        val owner: DiscordUser,
        val summary: String,
        @SerialName("verify_key")
        val verifyKey: String,
        val team: DiscordTeam?,
        @SerialName("guild_id")
        val guildId: String? = null,
        @SerialName("primary_sku_id")
        val primarySkuId: String? = null,
        val slug: String? = null,
        @SerialName("cover_image")
        val coverImage: String? = null
)

package com.gitlab.kordlib.rest.json.response

import com.gitlab.kordlib.common.entity.*
import kotlinx.serialization.*
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.serializer
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.internal.IntDescriptor
import kotlinx.serialization.json.*
import mu.KotlinLogging

private val auditLogger = KotlinLogging.logger { }

@Serializable
data class AuditLogResponse(
        val webhooks: List<DiscordWebhook>,
        val users: List<DiscordUser>,
        @SerialName("audit_log_entries")
        val auditLogEntries: List<AuditLogEntryResponse>
)

@Serializable
data class AuditLogEntryResponse(
        @SerialName("target_id")
        val targetId: String?,
        val changes: List<AuditLogChangeResponse<out @Contextual Any?>>? = null,
        @SerialName("user_id")
        val userId: String,
        val id: String,
        @SerialName("action_type")
        val actionType: AuditLogEventResponse,
        val options: AuditEntryInfoResponse? = null,
        val reason: String? = null
)

@Serializable(with = AuditLogChangeResponse.AuditLogChangeSerializer::class)
sealed class AuditLogChangeResponse<T> {
    abstract val old: T?
    abstract val new: T?

    @Serializer(forClass = AuditLogChangeResponse::class)
    companion object AuditLogChangeSerializer : KSerializer<AuditLogChangeResponse<*>> {
        override val descriptor: SerialDescriptor = buildClassSerialDescriptor("AuditLogChange") {
            element("key", String.serializer().descriptor)
            element("old_value", JsonElement.serializer().descriptor, isOptional = true)
            element("new_value", JsonElement.serializer().descriptor, isOptional = true)

        }

        override fun deserialize(decoder: Decoder): AuditLogChangeResponse<*> {
            lateinit var key: String
            var new: JsonElement? = null
            var old: JsonElement? = null
            with(decoder.beginStructure(descriptor)) {
                loop@ while (true) {
                    when (val index = decodeElementIndex(descriptor)) {
                        CompositeDecoder.DECODE_DONE -> break@loop
                        0 -> key = decodeStringElement(descriptor, index)
                        1 -> old = decodeSerializableElement(descriptor, index, JsonElement.serializer())
                        2 -> new = decodeSerializableElement(descriptor, index, JsonElement.serializer())
                    }
                }
                endStructure(descriptor)
            }
            return when (key) {
                "name" -> NameLogChange(old?.jsonPrimitive?.content, new?.jsonPrimitive?.content)
                "icon_hash" -> IconHashLogChange(old?.jsonPrimitive?.content, new?.jsonPrimitive?.content)
                "splash_hash" -> SplashHashLogChange(old?.jsonPrimitive?.content, new?.jsonPrimitive?.content)
                "owner_id" -> OwnerLogChange(old?.jsonPrimitive?.content, new?.jsonPrimitive?.content)
                "region" -> RegionLogChange(old?.jsonPrimitive?.content, new?.jsonPrimitive?.content)
                "afk_channel_id" -> AFKChannelLogChange(old?.jsonPrimitive?.content, new?.jsonPrimitive?.content)
                "vanity_url_code" -> VanityUrlLogChange(old?.jsonPrimitive?.content, new?.jsonPrimitive?.content)
                "widget_channel_id" -> WidgetChannelLogChange(old?.jsonPrimitive?.content, new?.jsonPrimitive?.content)
                "application_id" -> ApplicationLogChange(old?.jsonPrimitive?.content, new?.jsonPrimitive?.content)
                "code" -> CodeLogChange(old?.jsonPrimitive?.content, new?.jsonPrimitive?.content)
                "channel_id" -> ChannelLogChange(old?.jsonPrimitive?.content, new?.jsonPrimitive?.content)
                "inviter_id" -> InviterLogChange(old?.jsonPrimitive?.content, new?.jsonPrimitive?.content)
                "nick" -> NickLogChange(old?.jsonPrimitive?.content, new?.jsonPrimitive?.content)
                "topic" -> TopicLogChange(old?.jsonPrimitive?.content, new?.jsonPrimitive?.content)
                "avatar_hash" -> AvatarHashLogChange(old?.jsonPrimitive?.content, new?.jsonPrimitive?.content)
                "id" -> IdLogChange(old?.jsonPrimitive?.content, new?.jsonPrimitive?.content)

                "widget_enabled" -> WidgetEnabledLogChange(old?.jsonPrimitive?.boolean, new?.jsonPrimitive?.boolean)
                "nsfw" -> NSFWLogChange(old?.jsonPrimitive?.boolean, new?.jsonPrimitive?.boolean)
                "hoist" -> HoistLogChange(old?.jsonPrimitive?.boolean, new?.jsonPrimitive?.boolean)
                "mentionable" -> MentionableLogChange(old?.jsonPrimitive?.boolean, new?.jsonPrimitive?.boolean)
                "temporary" -> TemporaryLogChange(old?.jsonPrimitive?.boolean, new?.jsonPrimitive?.boolean)
                "deaf" -> DeafLogChange(old?.jsonPrimitive?.boolean, new?.jsonPrimitive?.boolean)
                "mute" -> MuteLogChange(old?.jsonPrimitive?.boolean, new?.jsonPrimitive?.boolean)

                "position" -> PositionLogChange(old?.jsonPrimitive?.int, new?.jsonPrimitive?.int)
                "max_uses" -> MaxUsesLogChange(old?.jsonPrimitive?.int, new?.jsonPrimitive?.int)
                "uses" -> UsesLogChange(old?.jsonPrimitive?.int, new?.jsonPrimitive?.int)
                "max_age" -> MaxAgeLogChange(old?.jsonPrimitive?.int, new?.jsonPrimitive?.int)
                "color" -> ColorLogChange(old?.jsonPrimitive?.int, new?.jsonPrimitive?.int)
                "bitrate" -> BitrateLogChange(old?.jsonPrimitive?.int, new?.jsonPrimitive?.int)
                "prune_delete_days" -> PruneDeleteDaysLogChange(old?.jsonPrimitive?.int, new?.jsonPrimitive?.int)
                "afk_timeout" -> AFKTimeoutLogChange(old?.jsonPrimitive?.int, new?.jsonPrimitive?.int)
                "explicit_content_filter" -> ExplicitContentFilterLogChange(old?.jsonPrimitive?.int, new?.jsonPrimitive?.int)
                "default_message_notifications" -> DefaultMessageNotificationLevelLogChange(old?.jsonPrimitive?.int, new?.jsonPrimitive?.int)
                "mfa_level" -> MFALogChange(old?.jsonPrimitive?.int, new?.jsonPrimitive?.int)
                "permissions" -> PermissionsLogChange(old?.jsonPrimitive?.int, new?.jsonPrimitive?.int)
                "allow" -> AllowLogChange(old?.jsonPrimitive?.int, new?.jsonPrimitive?.int)
                "deny" -> DenyLogChange(old?.jsonPrimitive?.int, new?.jsonPrimitive?.int)
                "verification_level" -> VerificationLevelLogChange(old?.jsonPrimitive?.int, new?.jsonPrimitive?.int)

                "\$remove" -> AddLogChange(listFromJson(DiscordAuditLogRoleChange.serializer(), old), listFromJson(DiscordAuditLogRoleChange.serializer(), new))
                "\$add" -> RemoveLogChange(listFromJson(DiscordAuditLogRoleChange.serializer(), old), listFromJson(DiscordAuditLogRoleChange.serializer(), new))
                "permission_overwrites" -> PermissionOverwriteLogChange(listFromJson(Overwrite.serializer(), old), listFromJson(Overwrite.serializer(), new))

                else -> Unknown

            }

        }

        private fun <T> listFromJson(serializer: KSerializer<T>, element: JsonElement?): List<T>? {
            return if (element != null) {
                val asListSerializer = ListSerializer(serializer)
                Json {
                    ignoreUnknownKeys = true
                    allowStructuredMapKeys = true
                    allowSpecialFloatingPointValues = true
                    isLenient = true
                }.decodeFromJsonElement(asListSerializer, element)
            } else null
        }
    }
}

data class HoistLogChange(override val old: Boolean?, override val new: Boolean?) : AuditLogChangeResponse<Boolean>()
data class NSFWLogChange(override val old: Boolean?, override val new: Boolean?) : AuditLogChangeResponse<Boolean>()
data class MentionableLogChange(override val old: Boolean?, override val new: Boolean?) : AuditLogChangeResponse<Boolean>()
data class TemporaryLogChange(override val old: Boolean?, override val new: Boolean?) : AuditLogChangeResponse<Boolean>()
data class DeafLogChange(override val old: Boolean?, override val new: Boolean?) : AuditLogChangeResponse<Boolean>()
data class MuteLogChange(override val old: Boolean?, override val new: Boolean?) : AuditLogChangeResponse<Boolean>()
data class WidgetEnabledLogChange(override val old: Boolean?, override val new: Boolean?) : AuditLogChangeResponse<Boolean>()
data class PermissionsLogChange(override val old: Permissions?, override val new: Permissions?) : AuditLogChangeResponse<Permissions>() {
    internal constructor(old: Int?, new: Int?) : this(old?.let { Permissions { +it } },
            new?.let { Permissions { +it } }
    )
}

data class AllowLogChange(override val old: Permissions?, override val new: Permissions?) : AuditLogChangeResponse<Permissions>() {
    internal constructor(old: Int?, new: Int?) : this(old?.let { Permissions { +it } },
            new?.let { Permissions { +it } }
    )
}

data class DenyLogChange(override val old: Permissions?, override val new: Permissions?) : AuditLogChangeResponse<Permissions>() {
    internal constructor(old: Int?, new: Int?) : this(old?.let { Permissions { +it } },
            new?.let { Permissions { +it } }
    )
}

data class NameLogChange(override val old: String?, override val new: String?) : AuditLogChangeResponse<String>()
data class VanityUrlLogChange(override val old: String?, override val new: String?) : AuditLogChangeResponse<String>()
data class SplashHashLogChange(override val old: String?, override val new: String?) : AuditLogChangeResponse<String>()
data class RegionLogChange(override val old: String?, override val new: String?) : AuditLogChangeResponse<String>()
data class AFKChannelLogChange(override val old: String?, override val new: String?) : AuditLogChangeResponse<String>()
data class WidgetChannelLogChange(override val old: String?, override val new: String?) : AuditLogChangeResponse<String>()
data class IconHashLogChange(override val old: String?, override val new: String?) : AuditLogChangeResponse<String>()
data class OwnerLogChange(override val old: String?, override val new: String?) : AuditLogChangeResponse<String>()
data class TopicLogChange(override val old: String?, override val new: String?) : AuditLogChangeResponse<String>()
data class ApplicationLogChange(override val old: String?, override val new: String?) : AuditLogChangeResponse<String>()
data class CodeLogChange(override val old: String?, override val new: String?) : AuditLogChangeResponse<String>()
data class ChannelLogChange(override val old: String?, override val new: String?) : AuditLogChangeResponse<String>()
data class InviterLogChange(override val old: String?, override val new: String?) : AuditLogChangeResponse<String>()
data class NickLogChange(override val old: String?, override val new: String?) : AuditLogChangeResponse<String>()
data class AvatarHashLogChange(override val old: String?, override val new: String?) : AuditLogChangeResponse<String>()
data class IdLogChange(override val old: String?, override val new: String?) : AuditLogChangeResponse<String>()
data class AFKTimeoutLogChange(override val old: Int?, override val new: Int?) : AuditLogChangeResponse<Int>()
data class PruneDeleteDaysLogChange(override val old: Int?, override val new: Int?) : AuditLogChangeResponse<Int>()
data class PositionLogChange(override val old: Int?, override val new: Int?) : AuditLogChangeResponse<Int>()
data class BitrateLogChange(override val old: Int?, override val new: Int?) : AuditLogChangeResponse<Int>()
data class MaxUsesLogChange(override val old: Int?, override val new: Int?) : AuditLogChangeResponse<Int>()
data class UsesLogChange(override val old: Int?, override val new: Int?) : AuditLogChangeResponse<Int>()
data class MaxAgeLogChange(override val old: Int?, override val new: Int?) : AuditLogChangeResponse<Int>()
data class ColorLogChange(override val old: Int?, override val new: Int?) : AuditLogChangeResponse<Int>()
data class AddLogChange(override val old: List<DiscordAuditLogRoleChange>?, override val new: List<DiscordAuditLogRoleChange>?) : AuditLogChangeResponse<List<DiscordAuditLogRoleChange>>()
data class RemoveLogChange(override val old: List<DiscordAuditLogRoleChange>?, override val new: List<DiscordAuditLogRoleChange>?) : AuditLogChangeResponse<List<DiscordAuditLogRoleChange>>()
data class PermissionOverwriteLogChange(override val old: List<Overwrite>?, override val new: List<Overwrite>?) : AuditLogChangeResponse<List<Overwrite>>()

data class MFALogChange(override val old: MFALevel?, override val new: MFALevel?) : AuditLogChangeResponse<MFALevel>() {
    internal constructor(old: Int?, new: Int?) : this(
            MFALevel.values().firstOrNull { it.code == new },
            MFALevel.values().firstOrNull { it.code == old }
    )
}

data class DefaultMessageNotificationLevelLogChange(override val old: DefaultMessageNotificationLevel?, override val new: DefaultMessageNotificationLevel?) : AuditLogChangeResponse<DefaultMessageNotificationLevel>() {
    internal constructor(old: Int?, new: Int?) : this(
            DefaultMessageNotificationLevel.values().firstOrNull { it.code == new },
            DefaultMessageNotificationLevel.values().firstOrNull { it.code == old }
    )
}

data class VerificationLevelLogChange(override val old: VerificationLevel?, override val new: VerificationLevel?) : AuditLogChangeResponse<VerificationLevel>() {
    internal constructor(old: Int?, new: Int?) : this(
            VerificationLevel.values().firstOrNull { it.code == new },
            VerificationLevel.values().firstOrNull { it.code == old }
    )
}

data class ExplicitContentFilterLogChange(override val old: ExplicitContentFilter?, override val new: ExplicitContentFilter?) : AuditLogChangeResponse<ExplicitContentFilter>() {
    internal constructor(old: Int?, new: Int?) : this(
            ExplicitContentFilter.values().firstOrNull { it.code == new },
            ExplicitContentFilter.values().firstOrNull { it.code == old }
    )
}

object Unknown : AuditLogChangeResponse<Nothing>() {
    override val old = null
    override val new = null
}

@Serializable(with = AuditLogEventResponse.AuditLogEventSerializer::class)
enum class AuditLogEventResponse(val code: Int) {
    /** The default code for unknown values. */
    Unknown(Int.MIN_VALUE),
    GuildUpdate(1),
    ChannelCreate(10),
    ChannelUpdate(11),
    ChannelDelete(12),
    ChannelOverwriteCreate(13),
    ChannelOverwriteUpdate(14),
    ChannelOverwriteDelete(15),

    MemberKick(20),
    MemberPrune(21),
    MemberBanAdd(22),
    MemberBanRemove(23),
    MemberUpdate(24),
    MemberRoleUpdate(25),
    MemberMove(26),
    MemberDisconnect(27),
    BotAdd(28),

    RoleCreate(30),
    RoleUpdate(31),
    RoleDelete(32),

    InviteCreate(40),
    InviteUpdate(41),
    InviteDelete(42),

    WebhookCreate(50),
    WebhookUpdate(51),
    WebhookDelete(52),

    EmojiCreate(60),
    EmojiUpdate(61),
    EmojiDelete(62),

    MessageDelete(72),
    MessageBulkDelete(73),
    MessagePin(74),
    MessageUnpin(75),

    IntegrationCreate(80),
    IntegrationUpdate(81),
    IntegrationDelete(82);

    @Serializer(forClass = AuditLogEventResponse::class)
    companion object AuditLogEventSerializer : KSerializer<AuditLogEventResponse> {
        override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("AuditLogEvent", PrimitiveKind.INT)

        override fun deserialize(decoder: Decoder): AuditLogEventResponse {
            val code = decoder.decodeInt()

            return values().firstOrNull { it.code == code } ?: Unknown
        }

        override fun serialize(encoder: Encoder, obj: AuditLogEventResponse) {
            encoder.encodeInt(obj.code)
        }

    }
}

@Serializable
data class AuditEntryInfoResponse(
        @SerialName("delete_member_days")
        val deleteMemberDays: String? = null,
        @SerialName("members_removed")
        val membersRemoved: String? = null,
        @SerialName("channel_id")
        val channelId: String? = null,
        @SerialName("message_id")
        val messageId: String? = null,
        val count: String? = null,
        val id: String? = null,
        val type: String? = null,
        @SerialName("role_name")
        val roleName: String? = null
)

package com.gitlab.kordlib.rest.json.response

import com.gitlab.kordlib.common.entity.DiscordUser
import kotlinx.serialization.Serializable

@Serializable
data class BanResponse(val reason: String?, val user: DiscordUser)

package com.gitlab.kordlib.rest.json.response

import com.gitlab.kordlib.common.entity.ChannelType
import kotlinx.serialization.Serializable

@Serializable
data class PartialChannelResponse(val name: String, val type: ChannelType)

package com.gitlab.kordlib.rest.json.response

import com.gitlab.kordlib.common.entity.DiscordGuildIntegrations
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class Connection(val id: String,
                      val name: String,
                      val type: String,
                      val revoked: Boolean,
                      val integrations: List<DiscordGuildIntegrations>,
                      val verified: Boolean,
                      @SerialName("friend_sync")
                      val friendSync: Boolean,
                      @SerialName("show_activity")
                      val showActivity: Boolean,
                      val visibility: Int)
//TODO add a visibility enum class

package com.gitlab.kordlib.rest.json.response

import com.gitlab.kordlib.rest.json.JsonErrorCode
import kotlinx.serialization.Serializable

@Serializable
class DiscordErrorResponse(val code: JsonErrorCode = JsonErrorCode.Unknown, val message: String = "")

package com.gitlab.kordlib.rest.json.response

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class EmbedResponse(val enabled: Boolean,
                         @SerialName("channel_id")
                         val channelId: String)

package com.gitlab.kordlib.rest.json.response

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class FollowedChannelResponse(
        @SerialName("channel_id")
        val channelId: String,
        @SerialName("webhook_id")
        val webhookId: String
)

package com.gitlab.kordlib.rest.json.response

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class GatewayResponse(val url: String, val shards: Int)

@Serializable
data class BotGatewayResponse(
        val url: String,
        val shards: Int,
        @SerialName("session_start_limit")
        val sessionStartLimit: SessionStartLimitResponse
)

@Serializable
data class SessionStartLimitResponse(
        val total: Int,
        val remaining: Int,
        @SerialName("reset_after")
        val resetAfter: Int
)

package com.gitlab.kordlib.rest.json.response

import com.gitlab.kordlib.common.entity.DiscordUser
import kotlinx.serialization.*
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
data class IntegrationResponse(
        val id: String,
        val name: String,
        val type: String,
        val enabled: Boolean,
        val syncing: Boolean,
        @SerialName("role_id")
        val roleId: String,
        @SerialName("enable_emoticons")
        val enableEmoticons: Boolean? = null,
        @SerialName("expire_behavior")
        val expireBehavior: IntegrationExpireBehavior,
        @SerialName("expire_grace_period")
        val expireGracePeriod: Int,
        val user: DiscordUser,
        val account: DiscordIntegrationsAccount,
        @SerialName("synced_at")
        val syncedAt: String
)

@Suppress("unused")
@Serializable(with = IntegrationExpireBehavior.Serializer::class)
enum class IntegrationExpireBehavior(val code: Int) {
    RemoveRole(0), Kick(1), Unknown(-1);

    companion object Serializer : KSerializer<IntegrationExpireBehavior> {
        override val descriptor: SerialDescriptor
            get() = PrimitiveSerialDescriptor("expire_behavior", PrimitiveKind.INT)

        override fun deserialize(decoder: Decoder): IntegrationExpireBehavior {
            val code = decoder.decodeInt()
            return values().firstOrNull { it.code == code } ?: Unknown
        }

        override fun serialize(encoder: Encoder, value: IntegrationExpireBehavior) {
            encoder.encodeInt(value.code)
        }

    }

}

@Serializable
data class DiscordIntegrationsAccount(val id: String, val name: String)

package com.gitlab.kordlib.rest.json.response

import com.gitlab.kordlib.common.entity.DiscordChannel
import com.gitlab.kordlib.common.entity.DiscordPartialGuild
import com.gitlab.kordlib.common.entity.DiscordUser
import com.gitlab.kordlib.common.entity.TargetUserType
import kotlinx.serialization.*
import kotlinx.serialization.internal.IntDescriptor

@Serializable
data class InviteResponse(
        val code: String,
        val guild: DiscordPartialGuild? = null,
        val channel: DiscordChannel,
        val inviter: DiscordUser? = null,
        @SerialName("target_user")
        val targetUser: DiscordUser? = null,
        @SerialName("target_user_type")
        val targetUserType: TargetUserType? = null,
        @SerialName("approximate_presence_count")
        val approximatePresenceCount: Int? = null,
        @SerialName("approximate_member_count")
        val approximateMemberCount: Int? = null,
        val uses: Int? = null
)

@Serializable
data class InviteMetaDataResponse(
        val inviter: DiscordUser,
        val uses: Int,
        @SerialName("max_uses")
        val maxUses: Int,
        @SerialName("max_age")
        val maxAge: Int,
        val temporary: Boolean,
        @SerialName("created_at")
        val createdAt: String
)

@Serializable
data class PartialInvite(val code: String? = null)

package com.gitlab.kordlib.rest.json.response

import kotlinx.serialization.Serializable

@Serializable
data class GetPruneResponse(val pruned: Int)

@Serializable
data class PruneResponse(val pruned: Int?)

package com.gitlab.kordlib.rest.json.response

import kotlinx.serialization.Serializable

@Serializable
data class VoiceRegion(val id: String, val name: String, val vip: Boolean, val optimal: Boolean, val deprecated: Boolean, val custom: Boolean)