From 6cfbf7281948a74c80777b9857af8bbb8c7f7be8 Mon Sep 17 00:00:00 2001 From: HopeBaron Date: Sun, 25 Apr 2021 22:06:54 +0300 Subject: [PATCH 01/15] Update to 1.5.0-RC --- buildSrc/src/main/kotlin/Dependencies.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index fb9475f42d2..eea2ceef887 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -1,5 +1,5 @@ object Versions { - const val kotlin = "1.4.32" + const val kotlin = "1.5.0-RC" const val kotlinxSerialization = "1.1.0" const val ktor = "1.5.2" const val kotlinxCoroutines = "1.4.2" From e1ef647721fe7f894045386c01d007bfd00dd090 Mon Sep 17 00:00:00 2001 From: HopeBaron Date: Sun, 25 Apr 2021 22:20:39 +0300 Subject: [PATCH 02/15] trigger on branch pushes --- .github/workflows/deployment-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/deployment-ci.yml b/.github/workflows/deployment-ci.yml index 44567004d1d..974568d2571 100644 --- a/.github/workflows/deployment-ci.yml +++ b/.github/workflows/deployment-ci.yml @@ -6,6 +6,8 @@ on: push: tags-ignore: - '**' # We don't want this to run on tags pushes + branches: + - '**' pull_request: release: types: [published] From e73721241610a85d184d3748e86b00a2d4bd2783 Mon Sep 17 00:00:00 2001 From: Michael Rittmeister Date: Tue, 27 Apr 2021 08:30:01 +0200 Subject: [PATCH 03/15] Port to Kotlin 1.5 (#268) * Port dependencies to Kotlin 1.5 - Convert AbstractRateLimiter.AbstractRequestToken to a static rather than an inner class due to a compiler bug - Downgrade kx.ser-json to 1.0.0 to avoid a compiler bug - Bump other Kotlin dependencies to latest fixup! Port dependencies to Kotlin 1.5 - Convert AbstractRateLimiter.AbstractRequestToken to a static rather than an inner class due to a compiler bug - Downgrade kx.ser-json to 1.0.0 to avoid a compiler bug - Bump other Kotlin dependencies to latest * Replace deprecated kotlin.time APIs * Replace more deprecated APIs & inline classes * Replace deprecated usage of time API in tests * Possibly fix test Co-authored-by: Hope <34831095+HopeBaron@users.noreply.github.com> --- build.gradle.kts | 1 + buildSrc/src/main/kotlin/Dependencies.kt | 10 +-- .../kotlin/ratelimit/BucketRateLimiter.kt | 2 +- .../kotlin/ratelimit/BucketRateLimiterTest.kt | 5 +- .../channel/MessageChannelBehavior.kt | 8 +- .../main/kotlin/builder/kord/KordBuilder.kt | 9 ++- .../kotlin/event/guild/InviteCreateEvent.kt | 2 +- core/src/main/kotlin/gateway/MasterGateway.kt | 4 +- .../kotlin/performance/KordEventDropTest.kt | 76 +++++++++---------- .../src/main/kotlin/DefaultGatewayBuilder.kt | 7 +- gateway/src/main/kotlin/retry/LinearRetry.kt | 11 +-- .../test/kotlin/gateway/DefaultGatewayTest.kt | 4 +- gateway/src/test/kotlin/json/CommandTest.kt | 3 +- .../kotlin/ratelimit/AbstractRateLimiter.kt | 46 ++++++----- .../ratelimit/ExclusionRequestRateLimiter.kt | 13 ++-- .../ratelimit/ParallelRequestRateLimiter.kt | 6 +- .../kotlin/ratelimit/RequestRateLimiter.kt | 12 ++- .../AbstractRequestRateLimiterTest.kt | 7 +- 18 files changed, 119 insertions(+), 107 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 95179e1b92d..a57cbff30fa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,6 +5,7 @@ import org.apache.commons.codec.binary.Base64 buildscript { repositories { jcenter() + mavenCentral() maven(url = "https://plugins.gradle.org/m2/") } dependencies { diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index eea2ceef887..db8f0d25d0a 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -1,10 +1,10 @@ object Versions { - const val kotlin = "1.5.0-RC" - const val kotlinxSerialization = "1.1.0" - const val ktor = "1.5.2" - const val kotlinxCoroutines = "1.4.2" + const val kotlin = "1.5.0" + const val kotlinxSerialization = "1.0.0" + const val ktor = "1.5.3" + const val kotlinxCoroutines = "1.4.3" const val kotlinLogging = "2.0.4" - const val atomicFu = "0.15.1" + const val atomicFu = "0.15.2" const val binaryCompatibilityValidator = "0.4.0" //test deps diff --git a/common/src/main/kotlin/ratelimit/BucketRateLimiter.kt b/common/src/main/kotlin/ratelimit/BucketRateLimiter.kt index 407b071a1b4..e613456b7dc 100644 --- a/common/src/main/kotlin/ratelimit/BucketRateLimiter.kt +++ b/common/src/main/kotlin/ratelimit/BucketRateLimiter.kt @@ -38,7 +38,7 @@ class BucketRateLimiter( private fun resetState() { count = 0 - nextInterval = clock.millis() + refillInterval.inMilliseconds.toLong() + nextInterval = clock.millis() + refillInterval.inWholeMilliseconds } private suspend fun delayUntilNextInterval() { diff --git a/common/src/test/kotlin/ratelimit/BucketRateLimiterTest.kt b/common/src/test/kotlin/ratelimit/BucketRateLimiterTest.kt index da970531a54..ba5407996ae 100644 --- a/common/src/test/kotlin/ratelimit/BucketRateLimiterTest.kt +++ b/common/src/test/kotlin/ratelimit/BucketRateLimiterTest.kt @@ -9,6 +9,7 @@ import java.time.ZoneOffset import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.asserter +import kotlin.time.Duration import kotlin.time.ExperimentalTime import kotlin.time.milliseconds @@ -16,7 +17,7 @@ import kotlin.time.milliseconds @ExperimentalCoroutinesApi class BucketRateLimiterTest { - val interval = 1_000_000.milliseconds + val interval = Duration.milliseconds(1_000_000) val instant = Instant.now() val clock = Clock.fixed(instant, ZoneOffset.UTC) lateinit var rateLimiter: BucketRateLimiter @@ -38,7 +39,7 @@ class BucketRateLimiterTest { rateLimiter.consume() rateLimiter.consume() - asserter.assertTrue("expected timeout of ${interval.inMilliseconds.toLong()} ms but was $currentTime ms", interval.toLongMilliseconds() == currentTime) + asserter.assertTrue("expected timeout of ${interval.inWholeMilliseconds} ms but was $currentTime ms", interval.inWholeMilliseconds == currentTime) } } diff --git a/core/src/main/kotlin/behavior/channel/MessageChannelBehavior.kt b/core/src/main/kotlin/behavior/channel/MessageChannelBehavior.kt index df3e98301f7..21fbb951c22 100644 --- a/core/src/main/kotlin/behavior/channel/MessageChannelBehavior.kt +++ b/core/src/main/kotlin/behavior/channel/MessageChannelBehavior.kt @@ -24,8 +24,8 @@ import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract import kotlin.coroutines.coroutineContext +import kotlin.time.Duration import kotlin.time.TimeMark -import kotlin.time.seconds /** * The behavior of a Discord channel that can use messages. @@ -199,7 +199,7 @@ interface MessageChannelBehavior : ChannelBehavior, Strategizable { suspend fun typeUntil(mark: TimeMark) { while (mark.hasNotPassedNow()) { type() - delay(8.seconds.toLongMilliseconds()) //bracing ourselves for some network delays + delay(Duration.seconds(8).inWholeMilliseconds) //bracing ourselves for some network delays } } @@ -212,7 +212,7 @@ interface MessageChannelBehavior : ChannelBehavior, Strategizable { suspend fun typeUntil(instant: Instant) { while (instant.isBefore(Instant.now())) { type() - delay(8.seconds.toLongMilliseconds()) //bracing ourselves for some network delays + delay(Duration.seconds(8).inWholeMilliseconds) //bracing ourselves for some network delays } } @@ -297,7 +297,7 @@ suspend inline fun T.withTyping(block: T.() -> Unit kord.launch(context = coroutineContext) { while (typing) { type() - delay(8.seconds.toLongMilliseconds()) + delay(Duration.seconds(8).inWholeMilliseconds) } } diff --git a/core/src/main/kotlin/builder/kord/KordBuilder.kt b/core/src/main/kotlin/builder/kord/KordBuilder.kt index 3e3d987cb86..2f6edd0053e 100644 --- a/core/src/main/kotlin/builder/kord/KordBuilder.kt +++ b/core/src/main/kotlin/builder/kord/KordBuilder.kt @@ -40,18 +40,19 @@ import kotlin.concurrent.thread import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract +import kotlin.time.Duration import kotlin.time.seconds operator fun DefaultGateway.Companion.invoke( resources: ClientResources, - retry: Retry = LinearRetry(2.seconds, 60.seconds, 10) + retry: Retry = LinearRetry(Duration.seconds(2), Duration.seconds(60), 10) ): DefaultGateway { return DefaultGateway { url = "wss://gateway.discord.gg/" client = resources.httpClient reconnectRetry = retry - sendRateLimiter = BucketRateLimiter(120, 60.seconds) - identifyRateLimiter = BucketRateLimiter(1, 5.seconds) + sendRateLimiter = BucketRateLimiter(120, Duration.seconds(60)) + identifyRateLimiter = BucketRateLimiter(1, Duration.seconds(5)) } } @@ -63,7 +64,7 @@ class KordBuilder(val token: String) { private var shardsBuilder: (recommended: Int) -> Shards = { Shards(it) } private var gatewayBuilder: (resources: ClientResources, shards: List) -> List = { resources, shards -> - val rateLimiter = BucketRateLimiter(1, 5.seconds) + val rateLimiter = BucketRateLimiter(1, Duration.seconds(5)) shards.map { DefaultGateway { client = resources.httpClient diff --git a/core/src/main/kotlin/event/guild/InviteCreateEvent.kt b/core/src/main/kotlin/event/guild/InviteCreateEvent.kt index 9d333f007c1..5023577aaca 100644 --- a/core/src/main/kotlin/event/guild/InviteCreateEvent.kt +++ b/core/src/main/kotlin/event/guild/InviteCreateEvent.kt @@ -87,7 +87,7 @@ class InviteCreateEvent( /** * How long the invite is valid for (in seconds). */ - val maxAge: Duration get() = data.maxAge.seconds + val maxAge: Duration get() = Duration.seconds(data.maxAge) /** * The maximum number of times the invite can be used. diff --git a/core/src/main/kotlin/gateway/MasterGateway.kt b/core/src/main/kotlin/gateway/MasterGateway.kt index 72b2ce1afe5..09c7fa93e11 100644 --- a/core/src/main/kotlin/gateway/MasterGateway.kt +++ b/core/src/main/kotlin/gateway/MasterGateway.kt @@ -25,10 +25,10 @@ class MasterGateway( */ val averagePing get(): Duration? { - val pings = gateways.values.mapNotNull { it.ping.value?.inMicroseconds } + val pings = gateways.values.mapNotNull { it.ping.value?.inWholeMilliseconds } if (pings.isEmpty()) return null - return pings.average().microseconds + return Duration.microseconds(pings.average()) } diff --git a/core/src/test/kotlin/performance/KordEventDropTest.kt b/core/src/test/kotlin/performance/KordEventDropTest.kt index 39542aa3970..7656aa002e6 100644 --- a/core/src/test/kotlin/performance/KordEventDropTest.kt +++ b/core/src/test/kotlin/performance/KordEventDropTest.kt @@ -14,9 +14,9 @@ import dev.kord.rest.request.KtorRequestHandler import dev.kord.rest.service.RestClient import io.ktor.client.* import kotlinx.coroutines.* -import kotlinx.coroutines.channels.BroadcastChannel -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import java.time.Clock import java.util.concurrent.CountDownLatch import java.util.concurrent.atomic.AtomicInteger @@ -25,7 +25,6 @@ import kotlin.coroutines.EmptyCoroutineContext import kotlin.test.Test import kotlin.test.assertEquals import kotlin.time.Duration -import kotlin.time.minutes class KordEventDropTest { @@ -47,13 +46,13 @@ class KordEventDropTest { } val kord = Kord( - resources = ClientResources("token", Shards(1), HttpClient(), EntitySupplyStrategy.cache, Intents.none), - cache = DataCache.none(), - MasterGateway(mapOf(0 to SpammyGateway)), - RestClient(KtorRequestHandler("token", clock = Clock.systemUTC())), - Snowflake("420"), - MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE), - Dispatchers.Default + resources = ClientResources("token", Shards(1), HttpClient(), EntitySupplyStrategy.cache, Intents.none), + cache = DataCache.none(), + MasterGateway(mapOf(0 to SpammyGateway)), + RestClient(KtorRequestHandler("token", clock = Clock.systemUTC())), + Snowflake("420"), + MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE), + Dispatchers.Default ) @Test @@ -61,32 +60,33 @@ class KordEventDropTest { val amount = 1_000 val event = GuildCreate( - DiscordGuild( - Snowflake("1337"), - "discord guild", - afkTimeout = 0, - defaultMessageNotifications = DefaultMessageNotificationLevel.AllMessages, - emojis = emptyList(), - explicitContentFilter = ExplicitContentFilter.AllMembers, - features = emptyList(), - mfaLevel = MFALevel.Elevated, - ownerId = Snowflake("123"), - preferredLocale = "en", - description = "A not really real guild", - premiumTier = PremiumTier.None, - region = "idk", - roles = emptyList(), - verificationLevel = VerificationLevel.High, - icon = null, - afkChannelId = null, - applicationId = null, - systemChannelFlags = SystemChannelFlags(0), - systemChannelId = null, - rulesChannelId = null, - vanityUrlCode = null, - banner = null, - publicUpdatesChannelId = null - ), 0) + DiscordGuild( + Snowflake("1337"), + "discord guild", + afkTimeout = 0, + defaultMessageNotifications = DefaultMessageNotificationLevel.AllMessages, + emojis = emptyList(), + explicitContentFilter = ExplicitContentFilter.AllMembers, + features = emptyList(), + mfaLevel = MFALevel.Elevated, + ownerId = Snowflake("123"), + preferredLocale = "en", + description = "A not really real guild", + premiumTier = PremiumTier.None, + region = "idk", + roles = emptyList(), + verificationLevel = VerificationLevel.High, + icon = null, + afkChannelId = null, + applicationId = null, + systemChannelFlags = SystemChannelFlags(0), + systemChannelId = null, + rulesChannelId = null, + vanityUrlCode = null, + banner = null, + publicUpdatesChannelId = null + ), 0 + ) val counter = AtomicInteger(0) val countdown = CountDownLatch(amount) @@ -99,7 +99,7 @@ class KordEventDropTest { SpammyGateway.events.emit(event) } - withTimeout(1.minutes) { + withTimeout(Duration.minutes(1).inWholeMilliseconds) { countdown.await() } assertEquals(amount, counter.get()) diff --git a/gateway/src/main/kotlin/DefaultGatewayBuilder.kt b/gateway/src/main/kotlin/DefaultGatewayBuilder.kt index f4d521a0533..5305a78640e 100644 --- a/gateway/src/main/kotlin/DefaultGatewayBuilder.kt +++ b/gateway/src/main/kotlin/DefaultGatewayBuilder.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ObsoleteCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow +import kotlin.time.Duration import kotlin.time.seconds class DefaultGatewayBuilder { @@ -32,9 +33,9 @@ class DefaultGatewayBuilder { 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) + val retry = reconnectRetry ?: LinearRetry(Duration.seconds(2), Duration.seconds(20), 10) + val sendRateLimiter = sendRateLimiter ?: BucketRateLimiter(120, Duration.seconds(60)) + val identifyRateLimiter = identifyRateLimiter ?: BucketRateLimiter(1, Duration.seconds(5)) client.requestPipeline.intercept(HttpRequestPipeline.Render) { // CIO adds this header even if no extensions are used, which causes it to be empty diff --git a/gateway/src/main/kotlin/retry/LinearRetry.kt b/gateway/src/main/kotlin/retry/LinearRetry.kt index bcbfd080b50..dcf08181681 100644 --- a/gateway/src/main/kotlin/retry/LinearRetry.kt +++ b/gateway/src/main/kotlin/retry/LinearRetry.kt @@ -22,15 +22,12 @@ class LinearRetry constructor( 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(firstBackoff.isPositive()) { "firstBackoff needs to be positive but was ${firstBackoff.inWholeMilliseconds} ms" } + require(maxBackoff.isPositive()) { "maxBackoff needs to be positive but was ${maxBackoff.inWholeMilliseconds} ms" } require( maxBackoff.minus(firstBackoff).isPositive() - ) { "maxBackoff ${maxBackoff.toLongMilliseconds()} ms needs to be bigger than firstBackoff ${firstBackoff.toLongMilliseconds()} ms" } + ) { "maxBackoff ${maxBackoff.inWholeMilliseconds} ms needs to be bigger than firstBackoff ${firstBackoff.inWholeMilliseconds} ms" } require(maxTries > 0) { "maxTries needs to be positive but was $maxTries" } } @@ -47,7 +44,7 @@ class LinearRetry constructor( if (!hasNext) error("max retries exceeded") tries.incrementAndGet() - var diff = (maxBackoff - firstBackoff).toLongMilliseconds() / maxTries + var diff = (maxBackoff - firstBackoff).inWholeMilliseconds / maxTries diff *= tries.value linearRetryLogger.trace { "retry attempt ${tries.value}, delaying for $diff ms" } delay(diff) diff --git a/gateway/src/test/kotlin/gateway/DefaultGatewayTest.kt b/gateway/src/test/kotlin/gateway/DefaultGatewayTest.kt index 2f1476500c4..d5b4ed6c261 100644 --- a/gateway/src/test/kotlin/gateway/DefaultGatewayTest.kt +++ b/gateway/src/test/kotlin/gateway/DefaultGatewayTest.kt @@ -22,7 +22,7 @@ import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import java.time.Duration import kotlin.time.ExperimentalTime -import kotlin.time.seconds +import kotlin.time.Duration as KDuration import kotlin.time.toKotlinDuration @FlowPreview @@ -44,7 +44,7 @@ class DefaultGatewayTest { } } - reconnectRetry = LinearRetry(2.seconds, 20.seconds, 10) + reconnectRetry = LinearRetry(KDuration.seconds(2), KDuration.seconds(20), 10) sendRateLimiter = BucketRateLimiter(120, Duration.ofSeconds(60).toKotlinDuration()) } diff --git a/gateway/src/test/kotlin/json/CommandTest.kt b/gateway/src/test/kotlin/json/CommandTest.kt index 69c736f6e57..167bc76d80b 100644 --- a/gateway/src/test/kotlin/json/CommandTest.kt +++ b/gateway/src/test/kotlin/json/CommandTest.kt @@ -13,6 +13,7 @@ import dev.kord.gateway.* import kotlinx.serialization.json.* import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test +import java.util.* private val json = Json { encodeDefaults = false } @@ -112,7 +113,7 @@ class CommandTest { put("d", buildJsonObject { put("since", since) put("activities", null as String?) - put("status", status.value.toLowerCase()) + put("status", status.value.lowercase(Locale.getDefault())) put("afk", afk) }) }) diff --git a/rest/src/main/kotlin/ratelimit/AbstractRateLimiter.kt b/rest/src/main/kotlin/ratelimit/AbstractRateLimiter.kt index 4e612be6b4f..41421dfc642 100644 --- a/rest/src/main/kotlin/ratelimit/AbstractRateLimiter.kt +++ b/rest/src/main/kotlin/ratelimit/AbstractRateLimiter.kt @@ -11,15 +11,15 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.sync.Mutex import mu.KLogger import java.time.Clock +import kotlin.time.Duration as KDuration 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 autoBanRateLimiter = BucketRateLimiter(25000, KDuration.minutes(10)) internal val globalSuspensionPoint = atomic(Reset(clock.instant())) internal val buckets = ConcurrentHashMap() internal val routeBuckets = ConcurrentHashMap>() @@ -45,7 +45,8 @@ abstract class AbstractRateLimiter internal constructor(val clock: Clock) : Requ internal abstract fun newToken(request: Request<*, *>, buckets: List): RequestToken - internal abstract inner class AbstractRequestToken( + internal abstract class AbstractRequestToken( + val rateLimiter: AbstractRateLimiter, val identity: RequestIdentifier, val requestBuckets: List ) : RequestToken { @@ -54,29 +55,32 @@ abstract class AbstractRateLimiter internal constructor(val clock: Clock) : Requ 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 - } - } + override suspend fun complete(response: RequestResponse) { + with(rateLimiter) { + val key = response.bucketKey + if (key != null) { + if (identity.addBucket(key)) { - when (response) { - is RequestResponse.GlobalRateLimit -> { - logger.trace { "[RATE LIMIT]:[GLOBAL]:exhausted until ${response.reset.value}" } - globalSuspensionPoint.update { response.reset } + logger.trace { "[DISCOVERED]:[BUCKET]:Bucket discovered for" } + buckets[key] = key.bucket + } } - is RequestResponse.BucketRateLimit -> { - logger.trace { "[RATE LIMIT]:[BUCKET]:Bucket ${response.bucketKey.value} was exhausted until ${response.reset.value}" } - response.bucketKey.bucket.updateReset(response.reset) + + 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() } + completableDeferred.complete(Unit) + requestBuckets.forEach { it.unlock() } + } } - } internal inner class Bucket(val id: BucketKey) { diff --git a/rest/src/main/kotlin/ratelimit/ExclusionRequestRateLimiter.kt b/rest/src/main/kotlin/ratelimit/ExclusionRequestRateLimiter.kt index 1a0d1331622..d8b5dacf8ac 100644 --- a/rest/src/main/kotlin/ratelimit/ExclusionRequestRateLimiter.kt +++ b/rest/src/main/kotlin/ratelimit/ExclusionRequestRateLimiter.kt @@ -3,13 +3,10 @@ package dev.kord.rest.ratelimit import dev.kord.rest.request.Request import dev.kord.rest.request.RequestIdentifier import dev.kord.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 {} @@ -31,11 +28,15 @@ class ExclusionRequestRateLimiter(clock: Clock = Clock.systemUTC()) : AbstractRa } override fun newToken(request: Request<*, *>, buckets: List): RequestToken { - return ExclusionRequestToken(request.identifier, buckets) + return ExclusionRequestToken(this, request.identifier, buckets) } - private inner class ExclusionRequestToken(identity: RequestIdentifier, requestBuckets: List) : - AbstractRequestToken(identity, requestBuckets) { + private inner class ExclusionRequestToken( + rateLimiter: ExclusionRequestRateLimiter, + identity: RequestIdentifier, + requestBuckets: List + ) : + AbstractRequestToken(rateLimiter, identity, requestBuckets) { override suspend fun complete(response: RequestResponse) { super.complete(response) diff --git a/rest/src/main/kotlin/ratelimit/ParallelRequestRateLimiter.kt b/rest/src/main/kotlin/ratelimit/ParallelRequestRateLimiter.kt index 54e5f61b760..2774f82dfdc 100644 --- a/rest/src/main/kotlin/ratelimit/ParallelRequestRateLimiter.kt +++ b/rest/src/main/kotlin/ratelimit/ParallelRequestRateLimiter.kt @@ -30,9 +30,9 @@ class ParallelRequestRateLimiter(clock: Clock = Clock.systemUTC()) : AbstractRat get() = parallelLogger override fun newToken(request: Request<*, *>, buckets: List): RequestToken = - ParallelRequestToken(request.identifier, buckets) + ParallelRequestToken(this, request.identifier, buckets) - private inner class ParallelRequestToken(identity: RequestIdentifier, requestBuckets: List) : - AbstractRequestToken(identity, requestBuckets) + private inner class ParallelRequestToken(rateLimiter: ParallelRequestRateLimiter, identity: RequestIdentifier, requestBuckets: List) : + AbstractRequestToken(rateLimiter, identity, requestBuckets) } \ No newline at end of file diff --git a/rest/src/main/kotlin/ratelimit/RequestRateLimiter.kt b/rest/src/main/kotlin/ratelimit/RequestRateLimiter.kt index 106f5ee951a..9e7dc8a424f 100644 --- a/rest/src/main/kotlin/ratelimit/RequestRateLimiter.kt +++ b/rest/src/main/kotlin/ratelimit/RequestRateLimiter.kt @@ -54,26 +54,30 @@ data class RateLimit(val total: Total, val remaining: Remaining) { companion object } -inline class Total(val value: Long) { +@JvmInline +value class Total(val value: Long) { companion object } -inline class Remaining(val value: Long) { +@JvmInline +value class Remaining(val value: Long) { companion object } /** * The unique identifier of this bucket. */ -inline class BucketKey(val value: String) { +@JvmInline +value class BucketKey(val value: String) { companion object } /** * The [instant][value] when the current bucket gets reset. */ -inline class Reset(val value: Instant) { +@JvmInline +value class Reset(val value: Instant) { companion object } diff --git a/rest/src/test/kotlin/ratelimit/AbstractRequestRateLimiterTest.kt b/rest/src/test/kotlin/ratelimit/AbstractRequestRateLimiterTest.kt index 8d860a982ba..e6460977827 100644 --- a/rest/src/test/kotlin/ratelimit/AbstractRequestRateLimiterTest.kt +++ b/rest/src/test/kotlin/ratelimit/AbstractRequestRateLimiterTest.kt @@ -14,6 +14,7 @@ import java.time.ZoneOffset import kotlin.IllegalStateException import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.time.Duration import kotlin.time.ExperimentalTime import kotlin.time.seconds import kotlin.time.toJavaDuration @@ -24,7 +25,7 @@ abstract class AbstractRequestRateLimiterTest { abstract fun newRequestRateLimiter(clock: Clock) : RequestRateLimiter - private val timeout = 1000.seconds + private val timeout = Duration.seconds(1000) private val instant = Instant.EPOCH private val RateLimit.Companion.exhausted get() = RateLimit(Total(5), Remaining(0)) @@ -74,7 +75,7 @@ abstract class AbstractRequestRateLimiterTest { rateLimiter.sendRequest(clock, 1, rateLimit = RateLimit.exhausted) rateLimiter.sendRequest(clock, 1, rateLimit = RateLimit(Total(5), Remaining(5))) - assertEquals(timeout.inMilliseconds.toLong(), currentTime) + assertEquals(timeout.inWholeMilliseconds, currentTime) } @Test @@ -86,7 +87,7 @@ abstract class AbstractRequestRateLimiterTest { rateLimiter.sendRequest(clock, 2, 1 , rateLimit = RateLimit(Total(5), Remaining(5))) //discovery rateLimiter.sendRequest(clock, 2, 1, rateLimit = RateLimit(Total(5), Remaining(5))) - assertEquals(timeout.inMilliseconds.toLong(), currentTime) + assertEquals(timeout.inWholeMilliseconds, currentTime) } @Test From a95dd4a993632de8373b3115a329bfa4c48919a3 Mon Sep 17 00:00:00 2001 From: schlaubi Date: Wed, 5 May 2021 12:52:18 +0200 Subject: [PATCH 04/15] Port to Kotlin 1.5 - Bump dependencies to 1.5 recommended versions - Remove inline classes in favor of value classes - Add required opt-ins - Migrate some more deprecated apis --- build.gradle.kts | 9 ++++----- buildSrc/build.gradle.kts | 2 +- buildSrc/src/main/kotlin/Compiler.kt | 1 - buildSrc/src/main/kotlin/Dependencies.kt | 4 ++-- common/build.gradle.kts | 1 - core/build.gradle.kts | 1 - core/src/test/kotlin/KordTest.kt | 2 +- gateway/build.gradle.kts | 1 - gateway/src/main/kotlin/DefaultGateway.kt | 1 - .../main/kotlin/handler/HeartbeatHandler.kt | 6 ++++-- gateway/src/samples/kotlin/EventListener.kt | 20 +++++++++---------- .../test/kotlin/gateway/DefaultGatewayTest.kt | 3 ++- gradle/wrapper/gradle-wrapper.properties | 2 +- rest/build.gradle.kts | 1 - settings.gradle.kts | 1 + 15 files changed, 25 insertions(+), 30 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index a57cbff30fa..e025c8ef07e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -21,7 +21,7 @@ buildscript { plugins { id("org.jetbrains.kotlin.jvm") version Versions.kotlin - id("org.jetbrains.dokka") version "1.4.0" + id("org.jetbrains.dokka") version "1.4.30" id("org.ajoberstar.git-publish") version "2.1.3" signing @@ -33,7 +33,6 @@ apply(plugin = "binary-compatibility-validator") repositories { mavenCentral() - jcenter() mavenLocal() } @@ -178,8 +177,8 @@ subprojects { if (!isJitPack) { repositories { maven { - url = if (Library.isSnapshot) uri(Repo.snapshotsUrl) - else uri(Repo.releasesUrl) +// url = if (Library.isSnapshot) uri(Repo.snapshotsUrl) +// else uri(Repo.releasesUrl) credentials { username = System.getenv("NEXUS_USER") @@ -219,7 +218,7 @@ tasks { dokkaHtmlMultiModule.configure { dependsOn(clean) outputDirectory.set(file(dokkaOutputDir)) - documentationFileName.set("DokkaDescription.md") +// documentationFileName.set("DokkaDescription.md") } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 03e1bb944d0..240797827ad 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -11,7 +11,7 @@ repositories { } dependencies { - implementation(kotlin("gradle-plugin-api", version = "1.4.0")) + implementation(kotlin("gradle-plugin-api", version = "1.5.0")) implementation(gradleApi()) implementation(localGroovy()) } \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/Compiler.kt b/buildSrc/src/main/kotlin/Compiler.kt index 102a1449365..237192819a2 100644 --- a/buildSrc/src/main/kotlin/Compiler.kt +++ b/buildSrc/src/main/kotlin/Compiler.kt @@ -1,5 +1,4 @@ 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" diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index db8f0d25d0a..96c440f0cbf 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -2,9 +2,9 @@ object Versions { const val kotlin = "1.5.0" const val kotlinxSerialization = "1.0.0" const val ktor = "1.5.3" - const val kotlinxCoroutines = "1.4.3" + const val kotlinxCoroutines = "1.5.0-RC" const val kotlinLogging = "2.0.4" - const val atomicFu = "0.15.2" + const val atomicFu = "0.16.1" const val binaryCompatibilityValidator = "0.4.0" //test deps diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 93413505262..24d2dca3bed 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -17,7 +17,6 @@ tasks.withType { kotlinOptions { jvmTarget = Jvm.target freeCompilerArgs = listOf( - CompilerArguments.inlineClasses, CompilerArguments.coroutines, CompilerArguments.time, CompilerArguments.optIn diff --git a/core/build.gradle.kts b/core/build.gradle.kts index c0005177513..00dbed06cd7 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -37,7 +37,6 @@ tasks.withType { kotlinOptions { jvmTarget = Jvm.target freeCompilerArgs = listOf( - CompilerArguments.inlineClasses, CompilerArguments.coroutines, CompilerArguments.time, CompilerArguments.stdLib, diff --git a/core/src/test/kotlin/KordTest.kt b/core/src/test/kotlin/KordTest.kt index a82ed4423b0..1b377e6928c 100644 --- a/core/src/test/kotlin/KordTest.kt +++ b/core/src/test/kotlin/KordTest.kt @@ -12,7 +12,7 @@ import java.util.concurrent.CountDownLatch internal class KordTest { @Test - @OptIn(KordExperimental::class) + @OptIn(KordExperimental::class, kotlinx.coroutines.DelicateCoroutinesApi::class) fun `Kord life cycle is correctly ended on shutdown`() { val kord = Kord.restOnly(System.getenv("KORD_TEST_TOKEN")) val lock = CountDownLatch(1) diff --git a/gateway/build.gradle.kts b/gateway/build.gradle.kts index a0ab67a0fb0..28b7028a7c2 100644 --- a/gateway/build.gradle.kts +++ b/gateway/build.gradle.kts @@ -27,7 +27,6 @@ tasks.withType { kotlinOptions { jvmTarget = Jvm.target freeCompilerArgs = listOf( - CompilerArguments.inlineClasses, CompilerArguments.coroutines, CompilerArguments.time, CompilerArguments.optIn diff --git a/gateway/src/main/kotlin/DefaultGateway.kt b/gateway/src/main/kotlin/DefaultGateway.kt index 61ece591a80..b9813834ade 100644 --- a/gateway/src/main/kotlin/DefaultGateway.kt +++ b/gateway/src/main/kotlin/DefaultGateway.kt @@ -59,7 +59,6 @@ data class DefaultGatewayData( /** * The default Gateway implementation of Kord, using an [HttpClient] for the underlying webSocket */ -@ObsoleteCoroutinesApi class DefaultGateway(private val data: DefaultGatewayData) : Gateway { override val coroutineContext: CoroutineContext = data.dispatcher + SupervisorJob() diff --git a/gateway/src/main/kotlin/handler/HeartbeatHandler.kt b/gateway/src/main/kotlin/handler/HeartbeatHandler.kt index 0f057a9738c..6e85bcdef94 100644 --- a/gateway/src/main/kotlin/handler/HeartbeatHandler.kt +++ b/gateway/src/main/kotlin/handler/HeartbeatHandler.kt @@ -5,9 +5,11 @@ import kotlinx.atomicfu.atomic import kotlinx.atomicfu.update import kotlinx.coroutines.ObsoleteCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlin.time.* +import kotlin.time.Duration +import kotlin.time.TimeMark +import kotlin.time.TimeSource -@ObsoleteCoroutinesApi +@OptIn(ObsoleteCoroutinesApi::class) internal class HeartbeatHandler( flow: Flow, private val send: suspend (Command) -> Unit, diff --git a/gateway/src/samples/kotlin/EventListener.kt b/gateway/src/samples/kotlin/EventListener.kt index 3497ac46fea..be089651b22 100644 --- a/gateway/src/samples/kotlin/EventListener.kt +++ b/gateway/src/samples/kotlin/EventListener.kt @@ -2,23 +2,21 @@ import dev.kord.common.entity.* import dev.kord.common.ratelimit.BucketRateLimiter import dev.kord.gateway.* import dev.kord.gateway.retry.LinearRetry -import io.ktor.client.HttpClient -import io.ktor.client.engine.cio.CIO -import io.ktor.client.features.json.JsonFeature +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.features.json.* import io.ktor.client.features.json.serializer.* -import io.ktor.client.features.websocket.WebSockets -import io.ktor.util.KtorExperimentalAPI +import io.ktor.client.features.websocket.* 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.json.Json -import java.time.Duration +import kotlin.time.Duration import kotlin.time.ExperimentalTime -import kotlin.time.seconds -import kotlin.time.toKotlinDuration +@OptIn(DelicateCoroutinesApi::class) @FlowPreview @ExperimentalTime @ExperimentalCoroutinesApi @@ -34,8 +32,8 @@ suspend fun main(args: Array) { } } - reconnectRetry = LinearRetry(2.seconds, 20.seconds, 10) - sendRateLimiter = BucketRateLimiter(120, Duration.ofSeconds(60).toKotlinDuration()) + reconnectRetry = LinearRetry(Duration.seconds(2), Duration.seconds(20), 10) + sendRateLimiter = BucketRateLimiter(120, Duration.seconds(60)) } gateway.events.filterIsInstance().flowOn(Dispatchers.Default).onEach { @@ -59,7 +57,7 @@ suspend fun main(args: Array) { afk = false, activities = listOf( DiscordBotActivity( - "Ping is ${gateway.ping.value?.toLongMilliseconds()}", + "Ping is ${gateway.ping.value?.inWholeMilliseconds}", ActivityType.Game ) ), diff --git a/gateway/src/test/kotlin/gateway/DefaultGatewayTest.kt b/gateway/src/test/kotlin/gateway/DefaultGatewayTest.kt index d5b4ed6c261..db73b1898ab 100644 --- a/gateway/src/test/kotlin/gateway/DefaultGatewayTest.kt +++ b/gateway/src/test/kotlin/gateway/DefaultGatewayTest.kt @@ -30,6 +30,7 @@ import kotlin.time.toKotlinDuration @ObsoleteCoroutinesApi @ExperimentalCoroutinesApi class DefaultGatewayTest { + @OptIn(DelicateCoroutinesApi::class) @Test @Disabled @ExperimentalTime @@ -57,7 +58,7 @@ class DefaultGatewayTest { "!status" -> when (words.getOrNull(1)) { "playing" -> gateway.send(UpdateStatus(status = PresenceStatus.Online, afk = false, activities = listOf(DiscordBotActivity("Kord", ActivityType.Game)), since = null)) } - "!ping" -> gateway.send(UpdateStatus(status = PresenceStatus.Online, afk = false, activities = listOf(DiscordBotActivity("Ping is ${gateway.ping.value?.toLongMilliseconds()}", ActivityType.Game)), since = null)) + "!ping" -> gateway.send(UpdateStatus(status = PresenceStatus.Online, afk = false, activities = listOf(DiscordBotActivity("Ping is ${gateway.ping.value?.inWholeMilliseconds}", ActivityType.Game)), since = null)) } }.launchIn(GlobalScope) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3758bba6e0e..851d0ca6f70 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ #Tue Dec 31 15:02:18 CET 2019 -distributionUrl=https\://services.gradle.org/distributions/gradle-6.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStorePath=wrapper/dists diff --git a/rest/build.gradle.kts b/rest/build.gradle.kts index 05e21b43a7f..7fa8ed96913 100644 --- a/rest/build.gradle.kts +++ b/rest/build.gradle.kts @@ -28,7 +28,6 @@ tasks.withType { kotlinOptions { jvmTarget = Jvm.target freeCompilerArgs = listOf( - CompilerArguments.inlineClasses, CompilerArguments.coroutines, CompilerArguments.time, CompilerArguments.optIn diff --git a/settings.gradle.kts b/settings.gradle.kts index 5c776ef82f6..cae69e5d4c9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -8,6 +8,7 @@ pluginManagement { } repositories { mavenLocal() + mavenCentral() maven(url = "https://dl.bintray.com/kotlin/kotlin-dev") gradlePluginPortal() } From 7602d8c4c7e044b1fb6de4d1b1310ca1400ada12 Mon Sep 17 00:00:00 2001 From: schlaubi Date: Wed, 5 May 2021 12:56:25 +0200 Subject: [PATCH 05/15] Fix some gradle issues --- build.gradle.kts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index e025c8ef07e..37dff56188d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -177,8 +177,8 @@ subprojects { if (!isJitPack) { repositories { maven { -// url = if (Library.isSnapshot) uri(Repo.snapshotsUrl) -// else uri(Repo.releasesUrl) + url = if (Library.isSnapshot) uri(Repo.snapshotsUrl) + else uri(Repo.releasesUrl) credentials { username = System.getenv("NEXUS_USER") @@ -218,6 +218,7 @@ tasks { dokkaHtmlMultiModule.configure { dependsOn(clean) outputDirectory.set(file(dokkaOutputDir)) + fileLayout.set(org.jetbrains.dokka.gradle.DokkaMultiModuleFileLayout.) // documentationFileName.set("DokkaDescription.md") } From 9d46595b4c0e16e1a2fa2b5b9182f5f085331a78 Mon Sep 17 00:00:00 2001 From: schlaubi Date: Wed, 5 May 2021 12:57:35 +0200 Subject: [PATCH 06/15] Fix Gradle compilation issue --- build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 37dff56188d..6f22d6da584 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -218,7 +218,6 @@ tasks { dokkaHtmlMultiModule.configure { dependsOn(clean) outputDirectory.set(file(dokkaOutputDir)) - fileLayout.set(org.jetbrains.dokka.gradle.DokkaMultiModuleFileLayout.) // documentationFileName.set("DokkaDescription.md") } From e1409676ea0d00dcffe5f66d8d4826d41d602edf Mon Sep 17 00:00:00 2001 From: schlaubi Date: Wed, 5 May 2021 21:24:39 +0200 Subject: [PATCH 07/15] Remove documentationFileName as the new dokka version doesn't support it anymore and there is no replacement yet --- build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 6f22d6da584..5acbc04134c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -218,7 +218,6 @@ tasks { dokkaHtmlMultiModule.configure { dependsOn(clean) outputDirectory.set(file(dokkaOutputDir)) -// documentationFileName.set("DokkaDescription.md") } From 23eded0cfe88eae94b41c1d111e176468d1a2b47 Mon Sep 17 00:00:00 2001 From: Michael Rittmeister Date: Wed, 12 May 2021 13:44:37 +0200 Subject: [PATCH 08/15] Port kotlinx.serialization to 1.2.0 (#279) * Port kotlinx.serialization to 1.2.0 - Convert local classes to top level classes (See Kotlin/kotlinx.serialization#1472) - Improve handling of empty JSON bodies (See Kotlin/kotlinx.serialization#678) - Fix Failing Command test * Fix failing test --- buildSrc/src/main/kotlin/Dependencies.kt | 2 +- .../entity/optional/OptionalBooleanTest.kt | 23 ++++---- .../kotlin/entity/optional/OptionalIntTest.kt | 24 ++++---- .../entity/optional/OptionalLongTest.kt | 30 +++++----- .../entity/optional/OptionalSnowflakeTest.kt | 26 +++++---- .../kotlin/entity/optional/OptionalTest.kt | 48 ++++++++------- .../kotlin/regression/CacheMissRegression.kt | 4 +- gateway/src/test/kotlin/json/CommandTest.kt | 27 +++++++-- .../main/kotlin/json/OptionalSerializer.kt | 35 ----------- .../main/kotlin/request/KtorRequestHandler.kt | 6 +- rest/src/main/kotlin/route/Route.kt | 58 ++++++++++++++----- 11 files changed, 151 insertions(+), 132 deletions(-) delete mode 100644 rest/src/main/kotlin/json/OptionalSerializer.kt diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index 96c440f0cbf..ec2f0019902 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -1,6 +1,6 @@ object Versions { const val kotlin = "1.5.0" - const val kotlinxSerialization = "1.0.0" + const val kotlinxSerialization = "1.2.0" const val ktor = "1.5.3" const val kotlinxCoroutines = "1.5.0-RC" const val kotlinLogging = "2.0.4" diff --git a/common/src/test/kotlin/entity/optional/OptionalBooleanTest.kt b/common/src/test/kotlin/entity/optional/OptionalBooleanTest.kt index 0d004196e76..14b32bf1d2b 100644 --- a/common/src/test/kotlin/entity/optional/OptionalBooleanTest.kt +++ b/common/src/test/kotlin/entity/optional/OptionalBooleanTest.kt @@ -10,41 +10,42 @@ import org.junit.jupiter.api.Test internal class OptionalBooleanTest { + @Serializable + private class EmptyOptionalEntity(val value: OptionalBoolean = OptionalBoolean.Missing) + @Test fun `deserializing nothing in optional assigns Missing`(){ @Language("json") val json = """{}""" - @Serializable - class Entity(val value: OptionalBoolean = OptionalBoolean.Missing) - - val entity = Json.decodeFromString(json) + val entity = Json.decodeFromString(json) assert(entity.value is OptionalBoolean.Missing) } + @Serializable + private class NullOptionalEntity(@Suppress("unused") val value: OptionalBoolean = OptionalBoolean.Missing) + @Test fun `deserializing null in optional throws SerializationException`(){ @Language("json") val json = """{ "value":null }""" - @Serializable - class Entity(@Suppress("unused") val value: OptionalBoolean = OptionalBoolean.Missing) - org.junit.jupiter.api.assertThrows { - Json.decodeFromString(json) + Json.decodeFromString(json) } } + @Serializable + private class ValueOptionalEntity(@Suppress("unused") val value: OptionalBoolean = OptionalBoolean.Missing) + @Test fun `deserializing value in optional assigns Value`(){ @Language("json") val json = """{ "value":true }""" - @Serializable - class Entity(@Suppress("unused") val value: OptionalBoolean = OptionalBoolean.Missing) - val entity = Json.decodeFromString(json) + val entity = Json.decodeFromString(json) require(entity.value is OptionalBoolean.Value) Assertions.assertEquals(true, entity.value.value) diff --git a/common/src/test/kotlin/entity/optional/OptionalIntTest.kt b/common/src/test/kotlin/entity/optional/OptionalIntTest.kt index c9d3990ab04..a6f497e26be 100644 --- a/common/src/test/kotlin/entity/optional/OptionalIntTest.kt +++ b/common/src/test/kotlin/entity/optional/OptionalIntTest.kt @@ -10,41 +10,43 @@ import org.junit.jupiter.api.Test internal class OptionalIntTest { + @Serializable + private class EmptyOptionalEntity(val value: OptionalInt = OptionalInt.Missing) + @Test fun `deserializing nothing in optional assigns Missing`(){ @Language("json") val json = """{}""" - @Serializable - class Entity(val value: OptionalInt = OptionalInt.Missing) - - val entity = Json.decodeFromString(json) + val entity = Json.decodeFromString(json) assert(entity.value is OptionalInt.Missing) } + + @Serializable + private class NullOptionalEntity(@Suppress("unused") val value: OptionalInt = OptionalInt.Missing) + @Test fun `deserializing null in optional throws SerializationException`(){ @Language("json") val json = """{ "value":null }""" - @Serializable - class Entity(@Suppress("unused") val value: OptionalInt = OptionalInt.Missing) org.junit.jupiter.api.assertThrows { - Json.decodeFromString(json) + Json.decodeFromString(json) } } + @Serializable + class ValueOptionalEntity(@Suppress("unused") val value: OptionalInt = OptionalInt.Missing) + @Test fun `deserializing value in optional assigns Value`(){ @Language("json") val json = """{ "value":5 }""" - @Serializable - class Entity(@Suppress("unused") val value: OptionalInt = OptionalInt.Missing) - - val entity = Json.decodeFromString(json) + val entity = Json.decodeFromString(json) require(entity.value is OptionalInt.Value) assertEquals(5, entity.value.value) diff --git a/common/src/test/kotlin/entity/optional/OptionalLongTest.kt b/common/src/test/kotlin/entity/optional/OptionalLongTest.kt index f5222c6525b..9ec0618be62 100644 --- a/common/src/test/kotlin/entity/optional/OptionalLongTest.kt +++ b/common/src/test/kotlin/entity/optional/OptionalLongTest.kt @@ -10,41 +10,45 @@ import org.junit.jupiter.api.Test internal class OptionalLongTest { + @Serializable + class EmptyOptionalEntity(val value: OptionalLong = OptionalLong.Missing) + @Test - fun `deserializing nothing in optional assigns Missing`(){ + fun `deserializing nothing in optional assigns Missing`() { @Language("json") val json = """{}""" - @Serializable - class Entity(val value: OptionalLong = OptionalLong.Missing) - val entity = Json.decodeFromString(json) + val entity = Json.decodeFromString(json) assert(entity.value is OptionalLong.Missing) } + + @Serializable + class NullOptionalEntity(@Suppress("unused") val value: OptionalLong = OptionalLong.Missing) + @Test - fun `deserializing null in optional throws SerializationException`(){ + fun `deserializing null in optional throws SerializationException`() { @Language("json") val json = """{ "value":null }""" - @Serializable - class Entity(@Suppress("unused") val value: OptionalLong = OptionalLong.Missing) org.junit.jupiter.api.assertThrows { - Json.decodeFromString(json) + Json.decodeFromString(json) } } + + @Serializable + class ValueOptionalEntity(@Suppress("unused") val value: OptionalLong = OptionalLong.Missing) + @Test - fun `deserializing value in optional assigns Value`(){ + fun `deserializing value in optional assigns Value`() { @Language("json") val json = """{ "value":5 }""" - @Serializable - class Entity(@Suppress("unused") val value: OptionalLong = OptionalLong.Missing) - - val entity = Json.decodeFromString(json) + val entity = Json.decodeFromString(json) require(entity.value is OptionalLong.Value) Assertions.assertEquals(5, entity.value.value) diff --git a/common/src/test/kotlin/entity/optional/OptionalSnowflakeTest.kt b/common/src/test/kotlin/entity/optional/OptionalSnowflakeTest.kt index d28cd31dfbb..948b8c3ce72 100644 --- a/common/src/test/kotlin/entity/optional/OptionalSnowflakeTest.kt +++ b/common/src/test/kotlin/entity/optional/OptionalSnowflakeTest.kt @@ -11,41 +11,45 @@ import org.junit.jupiter.api.Test internal class OptionalSnowflakeTest { + + @Serializable + class EmptyOptionalEntity(val value: OptionalSnowflake = OptionalSnowflake.Missing) + @Test fun `deserializing nothing in optional assigns Missing`(){ @Language("json") val json = """{}""" - @Serializable - class Entity(val value: OptionalSnowflake = OptionalSnowflake.Missing) - val entity = Json.decodeFromString(json) + val entity = Json.decodeFromString(json) assert(entity.value is OptionalSnowflake.Missing) } + + @Serializable + class NullOptionalEntity(@Suppress("unused") val value: OptionalSnowflake = OptionalSnowflake.Missing) + @Test fun `deserializing null in optional throws SerializationException`(){ @Language("json") val json = """{ "value":null }""" - @Serializable - class Entity(@Suppress("unused") val value: OptionalSnowflake = OptionalSnowflake.Missing) - org.junit.jupiter.api.assertThrows { - Json.decodeFromString(json) + Json.decodeFromString(json) } } + + @Serializable + class ValueOptionalEntity(@Suppress("unused") val value: OptionalSnowflake = OptionalSnowflake.Missing) + @Test fun `deserializing value in optional assigns Value`(){ @Language("json") val json = """{ "value":5 }""" - @Serializable - class Entity(@Suppress("unused") val value: OptionalSnowflake = OptionalSnowflake.Missing) - - val entity = Json.decodeFromString(json) + val entity = Json.decodeFromString(json) require(entity.value is OptionalSnowflake.Value) Assertions.assertEquals(Snowflake(5), entity.value.value) diff --git a/common/src/test/kotlin/entity/optional/OptionalTest.kt b/common/src/test/kotlin/entity/optional/OptionalTest.kt index b66e0d6f255..144a0387288 100644 --- a/common/src/test/kotlin/entity/optional/OptionalTest.kt +++ b/common/src/test/kotlin/entity/optional/OptionalTest.kt @@ -10,7 +10,7 @@ import org.junit.jupiter.api.Test internal class OptionalTest { @Test - fun `creating optional from nullable value returns Value on non-null value`(){ + fun `creating optional from nullable value returns Value on non-null value`() { val value: Int? = 5 val optional = Optional(value) @@ -19,62 +19,66 @@ internal class OptionalTest { } @Test - fun `creating optional from nullable value returns Null on null value`(){ + fun `creating optional from nullable value returns Null on null value`() { val value: Int? = null val optional = Optional(value) assert(optional is Optional.Null) } + + @Serializable + private class NullOptionalEntity(val value: Optional) + @Test - fun `deserializing null in nullable optional assigns Null`(){ + fun `deserializing null in nullable optional assigns Null`() { @Language("json") val json = """{ "value":null }""" - @Serializable - class Entity(val value: Optional) - - val entity = Json.decodeFromString(json) + val entity = Json.decodeFromString(json) assert(entity.value is Optional.Null) } + + @Serializable + class EmptyOptionalEntity(val value: Optional = Optional.Missing()) + @Test - fun `deserializing nothing in nullable optional assigns Missing`(){ + fun `deserializing nothing in nullable optional assigns Missing`() { @Language("json") val json = """{}""" - @Serializable - class Entity(val value: Optional = Optional.Missing()) - - val entity = Json.decodeFromString(json) + val entity = Json.decodeFromString(json) assert(entity.value is Optional.Missing) } + + @Serializable + class UnexpectedEmptyOptionalEntity(val value: Optional = Optional.Missing()) + @Test - fun `deserializing nothing in non-nullable optional assigns Missing`(){ + fun `deserializing nothing in non-nullable optional assigns Missing`() { @Language("json") val json = """{}""" - @Serializable - class Entity(val value: Optional = Optional.Missing()) - - val entity = Json.decodeFromString(json) + val entity = Json.decodeFromString(json) assert(entity.value is Optional.Missing) } + + @Serializable + private class UnexpectedNullOptionalEntity(@Suppress("unused") val value: Optional = Optional.Missing()) + @Test - fun `deserializing null in non-nullable optional throws SerializationException`(){ + fun `deserializing null in non-nullable optional throws SerializationException`() { @Language("json") val json = """{ "value":null }""" - @Serializable - class Entity(@Suppress("unused") val value: Optional = Optional.Missing()) - org.junit.jupiter.api.assertThrows { - Json.decodeFromString(json) + Json.decodeFromString(json) } } diff --git a/core/src/test/kotlin/regression/CacheMissRegression.kt b/core/src/test/kotlin/regression/CacheMissRegression.kt index 01a8bd89f40..e8678085b36 100644 --- a/core/src/test/kotlin/regression/CacheMissRegression.kt +++ b/core/src/test/kotlin/regression/CacheMissRegression.kt @@ -108,9 +108,7 @@ class CrashingHandler(val client: HttpClient) : RequestHandler { }.execute() - return parser.decodeFromString(request.route.strategy, response.readText()) - - + return request.route.mapper.deserialize(parser, response.readText()) } } diff --git a/gateway/src/test/kotlin/json/CommandTest.kt b/gateway/src/test/kotlin/json/CommandTest.kt index 167bc76d80b..25b9f8e7f87 100644 --- a/gateway/src/test/kotlin/json/CommandTest.kt +++ b/gateway/src/test/kotlin/json/CommandTest.kt @@ -2,6 +2,7 @@ package json +import dev.kord.common.entity.DiscordBotActivity import dev.kord.common.entity.DiscordShard import dev.kord.common.entity.PresenceStatus import dev.kord.common.entity.Snowflake @@ -61,7 +62,10 @@ class CommandTest { val query = "test" val limit = 1337 - val request = json.encodeToString(Command.Companion, RequestGuildMembers(Snowflake(guildId), query.optional(), OptionalInt.Value(limit))) + val request = json.encodeToString( + Command.Companion, + RequestGuildMembers(Snowflake(guildId), query.optional(), OptionalInt.Value(limit)) + ) val json = json.encodeToString(JsonObject.serializer(), buildJsonObject { put("op", OpCode.RequestGuildMembers.code) @@ -83,7 +87,10 @@ class CommandTest { val selfMute = true val selfDeaf = false - val status = json.encodeToString(Command.Companion, UpdateVoiceStatus(Snowflake(guildId), Snowflake(channelId), selfMute, selfDeaf)) + val status = json.encodeToString( + Command.Companion, + UpdateVoiceStatus(Snowflake(guildId), Snowflake(channelId), selfMute, selfDeaf) + ) val json = json.encodeToString(JsonObject.serializer(), buildJsonObject { put("op", OpCode.VoiceStateUpdate.code) @@ -102,7 +109,7 @@ class CommandTest { @Test fun `UpdateState command serialization`() { val since = 1242518400L - val game = null + val game = emptyList() val status = PresenceStatus.Online val afk = false @@ -112,7 +119,7 @@ class CommandTest { put("op", OpCode.StatusUpdate.code) put("d", buildJsonObject { put("since", since) - put("activities", null as String?) + put("activities", JsonArray(emptyList())) put("status", status.value.lowercase(Locale.getDefault())) put("afk", afk) }) @@ -133,8 +140,16 @@ class CommandTest { val presence: DiscordPresence? = null val identify = json.encodeToString( - Command.Companion, - Identify(token, properties, compress.optional(), largeThreshold.optionalInt(), shard.optional(), presence.optional().coerceToMissing(), Intents.all) + Command.Companion, + Identify( + token, + properties, + compress.optional(), + largeThreshold.optionalInt(), + shard.optional(), + presence.optional().coerceToMissing(), + Intents.all + ) ) val json = json.encodeToString(JsonObject.serializer(), buildJsonObject { diff --git a/rest/src/main/kotlin/json/OptionalSerializer.kt b/rest/src/main/kotlin/json/OptionalSerializer.kt deleted file mode 100644 index 207dd74b69c..00000000000 --- a/rest/src/main/kotlin/json/OptionalSerializer.kt +++ /dev/null @@ -1,35 +0,0 @@ -package dev.kord.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 [dev.kord.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 KSerializer.optional: KSerializer - get() = object : KSerializer { - - 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(this@optional, value) - } - } \ No newline at end of file diff --git a/rest/src/main/kotlin/request/KtorRequestHandler.kt b/rest/src/main/kotlin/request/KtorRequestHandler.kt index a3d9643bd37..6ca19fcb799 100644 --- a/rest/src/main/kotlin/request/KtorRequestHandler.kt +++ b/rest/src/main/kotlin/request/KtorRequestHandler.kt @@ -1,8 +1,8 @@ package dev.kord.rest.request -import dev.kord.rest.json.optional import dev.kord.rest.json.response.DiscordErrorResponse import dev.kord.rest.ratelimit.* +import dev.kord.rest.route.optional import io.ktor.client.* import io.ktor.client.engine.cio.* import io.ktor.client.features.* @@ -61,13 +61,13 @@ class KtorRequestHandler( if (response.contentType() == ContentType.Application.Json) throw KtorRequestException( response, - parser.decodeFromString(DiscordErrorResponse.serializer().optional, body) + DiscordErrorResponse.serializer().optional.deserialize(parser, body) ) else throw KtorRequestException(response, null) } else -> { logger.debug { response.logString(body) } - parser.decodeFromString(request.route.strategy, body) + request.route.mapper.deserialize(parser, body) } } } diff --git a/rest/src/main/kotlin/route/Route.kt b/rest/src/main/kotlin/route/Route.kt index 704953ff6f5..ed4891dfa3d 100644 --- a/rest/src/main/kotlin/route/Route.kt +++ b/rest/src/main/kotlin/route/Route.kt @@ -4,7 +4,6 @@ import dev.kord.common.annotation.DeprecatedSinceKord import dev.kord.common.annotation.KordExperimental import dev.kord.common.annotation.KordPreview import dev.kord.common.entity.* -import dev.kord.rest.json.optional import dev.kord.rest.json.response.* import io.ktor.http.* import kotlinx.serialization.DeserializationStrategy @@ -15,21 +14,48 @@ import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.StructureKind import kotlinx.serialization.descriptors.buildSerialDescriptor import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.json.Json import kotlinx.serialization.serializer import dev.kord.common.entity.DiscordEmoji as EmojiEntity internal const val REST_VERSION_PROPERTY_NAME = "dev.kord.rest.version" internal val restVersion get() = System.getenv(REST_VERSION_PROPERTY_NAME) ?: "v8" +sealed interface ResponseMapper { + fun deserialize(json: Json, body: String): T +} + +internal class ValueJsonMapper(val strategy: DeserializationStrategy) : ResponseMapper { + override fun deserialize(json: Json, body: String): T = json.decodeFromString(strategy, body) + override fun toString(): String = "ValueJsonMapper(strategy=$strategy)" +} + +internal class NullAwareMapper(val strategy: DeserializationStrategy) : ResponseMapper { + override fun deserialize(json: Json, body: String): T? { + if (body.isBlank()) return null + return json.decodeFromString(strategy, body) + } + + override fun toString(): String = "NullAwareMapper(strategy=$strategy)" +} + +internal val DeserializationStrategy.optional: ResponseMapper + get() = NullAwareMapper(this) + sealed class Route( val method: HttpMethod, val path: String, - val strategy: DeserializationStrategy + val mapper: ResponseMapper ) { + constructor( + method: HttpMethod, + path: String, + strategy: DeserializationStrategy + ) : this(method, path, ValueJsonMapper(strategy)) @OptIn(ExperimentalSerializationApi::class) override fun toString(): String = - "Route(method:${method.value},path:$path,strategy:${strategy.descriptor.serialName})" + "Route(method:${method.value},path:$path,mapper:$mapper)" object GatewayGet : Route(HttpMethod.Get, "/gateway", GatewayResponse.serializer()) @@ -425,7 +451,7 @@ sealed class Route( : Route(HttpMethod.Post, "/webhooks/$WebhookId/$WebhookToken/slack", NoStrategy) object ExecuteGithubWebhookPost - : Route(HttpMethod.Post, "/webhooks/$WebhookId/$WebhookToken", NoStrategy) + : Route(HttpMethod.Post, "/webhooks/$WebhookId/$WebhookToken/github", NoStrategy) object EditWebhookMessage : Route( HttpMethod.Patch, @@ -591,33 +617,33 @@ sealed class Route( @KordPreview object GuildApplicationCommandPermissionsGet : Route( - HttpMethod.Get, - "/applications/${ApplicationId}/guilds/$GuildId/commands/permissions", - DiscordGuildApplicationCommandPermissions.serializer() + HttpMethod.Get, + "/applications/${ApplicationId}/guilds/$GuildId/commands/permissions", + DiscordGuildApplicationCommandPermissions.serializer() ) @KordPreview object ApplicationCommandPermissionsGet : Route( - HttpMethod.Get, - "/applications/${ApplicationId}/guilds/$GuildId/commands/$CommandId/permissions", - DiscordGuildApplicationCommandPermissions.serializer() + HttpMethod.Get, + "/applications/${ApplicationId}/guilds/$GuildId/commands/$CommandId/permissions", + DiscordGuildApplicationCommandPermissions.serializer() ) @KordPreview object ApplicationCommandPermissionsPut : Route( - HttpMethod.Put, - "/applications/$ApplicationId/guilds/$GuildId/commands/$CommandId/permissions", - DiscordGuildApplicationCommandPermissions.serializer() + HttpMethod.Put, + "/applications/$ApplicationId/guilds/$GuildId/commands/$CommandId/permissions", + DiscordGuildApplicationCommandPermissions.serializer() ) @KordPreview object ApplicationCommandPermissionsBatchPut : Route>( - HttpMethod.Put, - "/applications/$ApplicationId/guilds/$GuildId/commands/permissions", - serializer() + HttpMethod.Put, + "/applications/$ApplicationId/guilds/$GuildId/commands/permissions", + serializer() ) object FollowupMessageCreate : Route( From 32c58d898fbac8480f7bf0b876ecf938a96aac91 Mon Sep 17 00:00:00 2001 From: Michael Rittmeister Date: Wed, 12 May 2021 23:41:35 +0200 Subject: [PATCH 09/15] Properly decode null in gateway events (#286) * Properly decode null in gateway events * Update gateway/src/main/kotlin/Event.kt Co-authored-by: Bart Arys Co-authored-by: Bart Arys --- gateway/src/main/kotlin/Event.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gateway/src/main/kotlin/Event.kt b/gateway/src/main/kotlin/Event.kt index 4fea9685a83..5e94a94f951 100644 --- a/gateway/src/main/kotlin/Event.kt +++ b/gateway/src/main/kotlin/Event.kt @@ -8,10 +8,7 @@ import dev.kord.common.entity.optional.OptionalSnowflake 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.descriptors.* import kotlinx.serialization.encoding.CompositeDecoder import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder @@ -31,6 +28,9 @@ private object NullDecoder : DeserializationStrategy { @OptIn(ExperimentalSerializationApi::class) override fun deserialize(decoder: Decoder): Nothing? { + // decodeNull() doesn't consume the literal null therefore parsing doesn't end and in e.g. a heartbeat event + // the null gets parsed as a key + decoder.decodeNotNullMark() return decoder.decodeNull() } From dc7cdd1badba144a719855b2f6401ea74c14d88b Mon Sep 17 00:00:00 2001 From: Michael Rittmeister Date: Fri, 14 May 2021 10:02:08 +0200 Subject: [PATCH 10/15] Update Kotlinx.serialization (#290) --- buildSrc/src/main/kotlin/Dependencies.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index ec2f0019902..6ae2e81f149 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -1,6 +1,6 @@ object Versions { const val kotlin = "1.5.0" - const val kotlinxSerialization = "1.2.0" + const val kotlinxSerialization = "1.2.1" const val ktor = "1.5.3" const val kotlinxCoroutines = "1.5.0-RC" const val kotlinLogging = "2.0.4" From d38dd7d22fc4e53e4d40e888eeed59ea3005789f Mon Sep 17 00:00:00 2001 From: Michael Rittmeister Date: Fri, 14 May 2021 13:06:08 +0200 Subject: [PATCH 11/15] Update Kotlin 1.5 branch to upstream (#292) * Implement voice stage channel (#239) * compute All * implement rest endpoints * JSON representation * implement core representation * handle stage channels * Apply suggestions Co-authored-by: BartArys * Remove duplicated factory function Co-authored-by: BartArys * add documentation * Document the requestToSpeak variable Co-authored-by: BartArys * Fix CI triggers * Add "Competing" activity type (Fix #270) (#272) * Make Updatestatus activities not-null (#274) As per Discord's documentation: https://github.com/discord/discord-api-docs/pull/2789 * Fix memory issues related to Permission combining (#277) * Do not octuple bitset size on copy the pure plus and minus function create a new array to work with, this incorrectly created an array of a size equal to the amount of bits that were allocated, instead the amount of longs. Thus, octupling the internal size. * Optimize Permission All The All Permission folded each DiscordBitSet of each value into eachother, resulting in n + 1 bitsets being created. This commit changes that to use the internal `add` which instead, which only mutates the single bitset created. * Add Stream permission It was missing * Add Permission All regression tests * Update deprecated message (#280) * Expose the creation of application commands behavior (#281) * Fix GuildUpdate core handling (#284) * Expose the creation of application commands behavior * Fix type of emitted event * Sealed message types (#282) * Expose the creation of application commands behavior * Make message types sealed * make Unknown a class * Add missing message types * make MessageTypeSerializer internal * Add buttons to Activity (#287) * Add buttons to Activity * Also pass buttons in constructor * Add missing fields to Guild (#288) * Add missing fields to Guild - Add welcome_screen - Add nsfw * Fix failing tests * Fix another failing tests * Add Message.applicationId (#289) Co-authored-by: Hope <34831095+HopeBaron@users.noreply.github.com> Co-authored-by: BartArys Co-authored-by: HopeBaron Co-authored-by: Bart Arys Co-authored-by: Noah Hendrickson --- .github/workflows/deployment-ci.yml | 2 + common/src/main/kotlin/DiscordBitSet.kt | 4 +- .../src/main/kotlin/entity/DiscordActivity.kt | 6 +- .../src/main/kotlin/entity/DiscordChannel.kt | 3 + common/src/main/kotlin/entity/DiscordGuild.kt | 7 +- .../src/main/kotlin/entity/DiscordMessage.kt | 78 ++++++++++++----- common/src/main/kotlin/entity/Permission.kt | 8 +- common/src/test/kotlin/json/GuildTest.kt | 1 + .../src/test/kotlin/json/PermissionsTest.kt | 17 +++- .../src/test/resources/json/guild/guild.json | 3 +- core/src/main/kotlin/Unsafe.kt | 17 ++++ .../GlobalApplicationCommandBehavior.kt | 33 +++++++ .../channel/BaseVoiceChannelBehavior.kt | 26 ++++++ .../behavior/channel/StageChannelBehavior.kt | 87 +++++++++++++++++++ .../behavior/channel/VoiceChannelBehavior.kt | 22 +---- .../main/kotlin/cache/data/ActivityData.kt | 4 +- core/src/main/kotlin/cache/data/GuildData.kt | 4 + .../src/main/kotlin/cache/data/MessageData.kt | 3 + .../main/kotlin/cache/data/VoiceStateData.kt | 7 +- core/src/main/kotlin/entity/Activity.kt | 3 + core/src/main/kotlin/entity/Guild.kt | 10 +++ core/src/main/kotlin/entity/Message.kt | 6 ++ core/src/main/kotlin/entity/VoiceState.kt | 6 +- .../src/main/kotlin/entity/channel/Channel.kt | 1 + .../entity/channel/StageVoiceChannel.kt | 54 ++++++++++++ .../event/channel/ChannelCreateEvent.kt | 7 ++ .../event/channel/ChannelDeleteEvent.kt | 6 ++ .../event/channel/ChannelUpdateEvent.kt | 7 ++ .../gateway/handler/ChannelEventHandler.kt | 3 + .../gateway/handler/GuildEventHandler.kt | 2 +- .../kotlin/performance/KordEventDropTest.kt | 54 ++++++------ gateway/src/main/kotlin/Command.kt | 2 +- .../main/kotlin/builder/PresenceBuilder.kt | 6 +- gateway/src/test/kotlin/json/CommandTest.kt | 4 +- .../channel/EditGuildChannelBuilder.kt | 30 +++++++ .../builder/guild/VoiceStateModifyBuilder.kt | 55 ++++++++++++ .../kotlin/json/request/VoiceStateRequests.kt | 24 +++++ rest/src/main/kotlin/route/Route.kt | 7 ++ .../src/main/kotlin/service/ChannelService.kt | 12 +++ rest/src/main/kotlin/service/GuildService.kt | 46 +++++++++- 40 files changed, 583 insertions(+), 94 deletions(-) create mode 100644 core/src/main/kotlin/behavior/channel/BaseVoiceChannelBehavior.kt create mode 100644 core/src/main/kotlin/behavior/channel/StageChannelBehavior.kt create mode 100644 core/src/main/kotlin/entity/channel/StageVoiceChannel.kt create mode 100644 rest/src/main/kotlin/builder/guild/VoiceStateModifyBuilder.kt create mode 100644 rest/src/main/kotlin/json/request/VoiceStateRequests.kt diff --git a/.github/workflows/deployment-ci.yml b/.github/workflows/deployment-ci.yml index 974568d2571..d808280dca4 100644 --- a/.github/workflows/deployment-ci.yml +++ b/.github/workflows/deployment-ci.yml @@ -4,6 +4,8 @@ name: Kotlin CI on: push: + branches: + - '**' # We want to run this on all branch pushes tags-ignore: - '**' # We don't want this to run on tags pushes branches: diff --git a/common/src/main/kotlin/DiscordBitSet.kt b/common/src/main/kotlin/DiscordBitSet.kt index 64b9ae9f885..1cdcf1028af 100644 --- a/common/src/main/kotlin/DiscordBitSet.kt +++ b/common/src/main/kotlin/DiscordBitSet.kt @@ -70,7 +70,7 @@ class DiscordBitSet(internal var data: LongArray) { } operator fun plus(another: DiscordBitSet): DiscordBitSet { - val dist = LongArray(size) + val dist = LongArray(data.size) data.copyInto(dist) val copy = DiscordBitSet(dist) copy.add(another) @@ -78,7 +78,7 @@ class DiscordBitSet(internal var data: LongArray) { } operator fun minus(another: DiscordBitSet): DiscordBitSet { - val dist = LongArray(size) + val dist = LongArray(data.size) data.copyInto(dist) val copy = DiscordBitSet(dist) copy.remove(another) diff --git a/common/src/main/kotlin/entity/DiscordActivity.kt b/common/src/main/kotlin/entity/DiscordActivity.kt index ea3fdecd3c6..20de507c4fd 100644 --- a/common/src/main/kotlin/entity/DiscordActivity.kt +++ b/common/src/main/kotlin/entity/DiscordActivity.kt @@ -35,7 +35,8 @@ data class DiscordActivity( val assets: Optional = Optional.Missing(), val secrets: Optional = Optional.Missing(), val instance: OptionalBoolean = OptionalBoolean.Missing, - val flags: Optional = Optional.Missing() + val flags: Optional = Optional.Missing(), + val buttons: Optional> = Optional.Missing() ) enum class ActivityFlag(val value: Int) { @@ -136,7 +137,8 @@ enum class ActivityType(val code: Int) { Streaming(1), Listening(2), Watching(3), - Custom(4); + Custom(4), + Competing(5); companion object ActivityTypeSerializer : KSerializer { override val descriptor: SerialDescriptor diff --git a/common/src/main/kotlin/entity/DiscordChannel.kt b/common/src/main/kotlin/entity/DiscordChannel.kt index 3d96a0c5e8c..5fb9f3f522d 100644 --- a/common/src/main/kotlin/entity/DiscordChannel.kt +++ b/common/src/main/kotlin/entity/DiscordChannel.kt @@ -94,6 +94,8 @@ sealed class ChannelType(val value: Int) { /** A channel in which game developers can sell their game on Discord. */ object GuildStore : ChannelType(6) + object GuildStageVoice : ChannelType(13) + companion object; internal object Serializer : KSerializer { @@ -108,6 +110,7 @@ sealed class ChannelType(val value: Int) { 4 -> GuildCategory 5 -> GuildNews 6 -> GuildStore + 13 -> GuildStageVoice else -> Unknown(code) } diff --git a/common/src/main/kotlin/entity/DiscordGuild.kt b/common/src/main/kotlin/entity/DiscordGuild.kt index 7893fdfba28..d063bdd162d 100644 --- a/common/src/main/kotlin/entity/DiscordGuild.kt +++ b/common/src/main/kotlin/entity/DiscordGuild.kt @@ -74,6 +74,7 @@ data class DiscordUnavailableGuild( * @param approximateMemberCount The approximate number of members in this guild, returned from the `GET /guild/` endpoint when `with_counts` is `true`. * @param approximatePresenceCount The approximate number of non-offline members in this guild, returned from the `GET /guild/` endpoint when `with_counts` is `true`. * @param welcomeScreen The welcome screen of a Community guild, shown to new members. + * @param nsfw true if this guild is [designated as NSFW](https://support.discord.com/hc/en-us/articles/1500005389362-NSFW-Server-Designation) */ @Serializable data class DiscordGuild( @@ -150,7 +151,9 @@ data class DiscordGuild( val approximateMemberCount: OptionalInt = OptionalInt.Missing, @SerialName("approximate_presence_count") val approximatePresenceCount: OptionalInt = OptionalInt.Missing, - + @SerialName("welcome_screen") + val welcomeScreen: Optional = Optional.Missing(), + val nsfw: Boolean ) /** @@ -372,6 +375,8 @@ data class DiscordVoiceState( @SerialName("self_stream") val selfStream: OptionalBoolean = OptionalBoolean.Missing, val suppress: Boolean, + @SerialName("request_to_speak_timestamp") + val requestToSpeakTimestamp: String? ) /** diff --git a/common/src/main/kotlin/entity/DiscordMessage.kt b/common/src/main/kotlin/entity/DiscordMessage.kt index a802889e7c8..e8f068c8aab 100644 --- a/common/src/main/kotlin/entity/DiscordMessage.kt +++ b/common/src/main/kotlin/entity/DiscordMessage.kt @@ -61,6 +61,7 @@ import kotlin.contracts.contract * @param flags Message flags. * @param stickers The stickers sent with the message (bots currently can only receive messages with stickers, not send). * @param referencedMessage the message associated with [messageReference]. + * @param applicationId if the message is a response to an [Interaction][DiscordInteraction], this is the id of the interaction's application */ @Serializable data class DiscordMessage( @@ -93,6 +94,8 @@ data class DiscordMessage( val type: MessageType, val activity: Optional = Optional.Missing(), val application: Optional = Optional.Missing(), + @SerialName("application_id") + val applicationId: OptionalSnowflake = OptionalSnowflake.Missing, @SerialName("message_reference") val messageReference: Optional = Optional.Missing(), val flags: Optional = Optional.Missing(), @@ -719,42 +722,77 @@ data class AllRemovedMessageReactions( ) @Serializable(with = MessageType.MessageTypeSerializer::class) -enum class MessageType(val code: Int) { +sealed 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), + class Unknown(code: Int) : MessageType(code) + object Default : MessageType(0) + object RecipientAdd : MessageType(1) + object RecipientRemove : MessageType(2) + object Call : MessageType(3) + object ChannelNameChange : MessageType(4) + object ChannelIconChange : MessageType(5) + object ChannelPinnedMessage : MessageType(6) + object GuildMemberJoin : MessageType(7) + object UserPremiumGuildSubscription : MessageType(8) + object UserPremiumGuildSubscriptionTierOne : MessageType(9) + object UserPremiumGuildSubscriptionTwo : MessageType(10) + object UserPremiumGuildSubscriptionThree : MessageType(11) + object ChannelFollowAdd : MessageType(12) + object GuildDiscoveryDisqualified : MessageType(14) @Suppress("SpellCheckingInspection") - GuildDiscoveryRequalified(15), - Reply(19); + object GuildDiscoveryRequalified : MessageType(15) + object GuildDiscoveryGracePeriodInitialWarning : MessageType(16) + object GuildDiscoveryGracePeriodFinalWarning : MessageType(17) + object ThreadCreated : MessageType(18) + object Reply : MessageType(19) + object ApplicationCommand : MessageType(20) + object ThreadStarterMessage : MessageType(21) + object GuildInviteReminder : MessageType(22) - companion object MessageTypeSerializer : KSerializer { + internal object MessageTypeSerializer : KSerializer { 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 + return values.firstOrNull { it.code == code } ?: Unknown(code) } override fun serialize(encoder: Encoder, value: MessageType) { encoder.encodeInt(value.code) } } + + companion object { + val values: Set + get() = setOf( + Default, + RecipientAdd, + RecipientRemove, + Call, + ChannelNameChange, + ChannelIconChange, + ChannelPinnedMessage, + GuildMemberJoin, + UserPremiumGuildSubscription, + UserPremiumGuildSubscriptionTierOne, + UserPremiumGuildSubscriptionTwo, + UserPremiumGuildSubscriptionThree, + ChannelFollowAdd, + GuildDiscoveryDisqualified, + GuildDiscoveryRequalified, + Reply, + GuildDiscoveryGracePeriodInitialWarning, + GuildDiscoveryGracePeriodFinalWarning, + ThreadCreated, + ApplicationCommand, + ThreadStarterMessage, + GuildInviteReminder, + + ) + } } @Serializable(with = AllowedMentionType.Serializer::class) diff --git a/common/src/main/kotlin/entity/Permission.kt b/common/src/main/kotlin/entity/Permission.kt index c23a1051763..f9cb20f3426 100644 --- a/common/src/main/kotlin/entity/Permission.kt +++ b/common/src/main/kotlin/entity/Permission.kt @@ -1,6 +1,7 @@ package dev.kord.common.entity import dev.kord.common.DiscordBitSet +import dev.kord.common.EmptyBitSet import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.PrimitiveKind @@ -18,7 +19,7 @@ class Permissions constructor(val code: DiscordBitSet) { /** * Returns this [Permissions] as a [Set] of [Permission] */ - val values = Permission.values.filter { it.code in code }.toSet() + val values = Permission.values.filter { it.code in code }.toSet() operator fun plus(permission: Permission): Permissions = Permissions(code + permission.code) @@ -130,6 +131,7 @@ sealed class Permission(val code: DiscordBitSet) { object ManageGuild : Permission(0x00000020) object AddReactions : Permission(0x00000040) object ViewAuditLog : Permission(0x00000080) + object Stream : Permission(0x00000200) object ViewChannel : Permission(0x00000400) object SendMessages : Permission(0x00000800) object SendTTSMessages : Permission(0x00001000) @@ -153,7 +155,8 @@ sealed class Permission(val code: DiscordBitSet) { object ManageWebhooks : Permission(0x20000000) object ManageEmojis : Permission(0x40000000) object UseSlashCommands : Permission(0x80000000) - object All : Permission(0xFFFFFDFF) + object RequestToSpeak : Permission(0x100000000) + object All : Permission(values.fold(EmptyBitSet()) { acc, value -> acc.add(value.code); acc }) companion object { val values: Set @@ -189,6 +192,7 @@ sealed class Permission(val code: DiscordBitSet) { ManageWebhooks, ManageEmojis, UseSlashCommands, + RequestToSpeak ) } } diff --git a/common/src/test/kotlin/json/GuildTest.kt b/common/src/test/kotlin/json/GuildTest.kt index 23b90127498..8880dcd394b 100644 --- a/common/src/test/kotlin/json/GuildTest.kt +++ b/common/src/test/kotlin/json/GuildTest.kt @@ -59,6 +59,7 @@ class GuildTest { preferredLocale shouldBe "en-US" rulesChannelId shouldBe "441688182833020939" publicUpdatesChannelId shouldBe "281283303326089216" + nsfw shouldBe true } } diff --git a/common/src/test/kotlin/json/PermissionsTest.kt b/common/src/test/kotlin/json/PermissionsTest.kt index 0d0efacfebf..63a7c8405d3 100644 --- a/common/src/test/kotlin/json/PermissionsTest.kt +++ b/common/src/test/kotlin/json/PermissionsTest.kt @@ -1,14 +1,27 @@ package json import dev.kord.common.DiscordBitSet -import dev.kord.common.entity.DiscordRole -import dev.kord.common.entity.Permissions +import dev.kord.common.EmptyBitSet +import dev.kord.common.entity.* import kotlinx.serialization.json.Json import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import org.junit.jupiter.api.Test class PermissionsTest { + + @Test + fun `adding permissions together does not swallow the universe`() { + Permission.values.fold(Permissions(DiscordBitSet(0))) { acc, permission -> + acc + permission + } + } + + @Test + fun `Permission All does not swallow the universe`() { + Permission.All //oh yeah, this is worthy of a test + } + @Test fun `permissions serialization test`() { val expected = buildJsonObject { diff --git a/common/src/test/resources/json/guild/guild.json b/common/src/test/resources/json/guild/guild.json index 0c7995a220d..a9f61a3f258 100644 --- a/common/src/test/resources/json/guild/guild.json +++ b/common/src/test/resources/json/guild/guild.json @@ -38,5 +38,6 @@ "system_channel_flags": 0, "preferred_locale": "en-US", "rules_channel_id": "441688182833020939", - "public_updates_channel_id": "281283303326089216" + "public_updates_channel_id": "281283303326089216", + "nsfw": true } \ No newline at end of file diff --git a/core/src/main/kotlin/Unsafe.kt b/core/src/main/kotlin/Unsafe.kt index dc3edb0017e..b09c1e206ba 100644 --- a/core/src/main/kotlin/Unsafe.kt +++ b/core/src/main/kotlin/Unsafe.kt @@ -6,6 +6,7 @@ import dev.kord.common.annotation.KordUnsafe import dev.kord.common.entity.Snowflake import dev.kord.core.behavior.* import dev.kord.core.behavior.channel.* +import dev.kord.rest.service.InteractionService /** * A class that exposes the creation of `{Entity}Behavior` classes. @@ -71,4 +72,20 @@ class Unsafe(private val kord: Kord) { return "Unsafe" } + fun guildApplicationCommand( + guildId: Snowflake, + applicationId: Snowflake, + commandId: Snowflake, + service: InteractionService = kord.rest.interaction + ): GuildApplicationCommandBehavior = + GuildApplicationCommandBehavior(guildId, applicationId, commandId, service) + + fun globalApplicationCommand( + applicationId: Snowflake, + commandId: Snowflake, + service: InteractionService = kord.rest.interaction + ): GlobalApplicationCommandBehavior = + GlobalApplicationCommandBehavior(applicationId, commandId, service) + + } \ No newline at end of file diff --git a/core/src/main/kotlin/behavior/GlobalApplicationCommandBehavior.kt b/core/src/main/kotlin/behavior/GlobalApplicationCommandBehavior.kt index 9c3168ed4a0..72a0d953b6e 100644 --- a/core/src/main/kotlin/behavior/GlobalApplicationCommandBehavior.kt +++ b/core/src/main/kotlin/behavior/GlobalApplicationCommandBehavior.kt @@ -79,4 +79,37 @@ interface GuildApplicationCommandBehavior : ApplicationCommandBehavior { override suspend fun delete() { service.deleteGuildApplicationCommand(applicationId, guildId, id) } + +} + +@KordPreview +fun GuildApplicationCommandBehavior( + guildId: Snowflake, + applicationId: Snowflake, + id: Snowflake, + service: InteractionService +): GuildApplicationCommandBehavior = object : GuildApplicationCommandBehavior { + override val guildId: Snowflake + get() = guildId + override val applicationId: Snowflake + get() = applicationId + override val service: InteractionService + get() = service + override val id: Snowflake + get() = id } + + +@KordPreview +fun GlobalApplicationCommandBehavior( + applicationId: Snowflake, + id: Snowflake, + service: InteractionService +): GlobalApplicationCommandBehavior = object : GlobalApplicationCommandBehavior { + override val applicationId: Snowflake + get() = applicationId + override val service: InteractionService + get() = service + override val id: Snowflake + get() = id +} \ No newline at end of file diff --git a/core/src/main/kotlin/behavior/channel/BaseVoiceChannelBehavior.kt b/core/src/main/kotlin/behavior/channel/BaseVoiceChannelBehavior.kt new file mode 100644 index 00000000000..4167f1ecc48 --- /dev/null +++ b/core/src/main/kotlin/behavior/channel/BaseVoiceChannelBehavior.kt @@ -0,0 +1,26 @@ +package dev.kord.core.behavior.channel + +import dev.kord.cache.api.query +import dev.kord.common.exception.RequestException +import dev.kord.core.cache.data.VoiceStateData +import dev.kord.core.cache.idEq +import dev.kord.core.entity.VoiceState +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +interface BaseVoiceChannelBehavior : 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 + get() = kord.cache.query { idEq(VoiceStateData::channelId, id) } + .asFlow() + .map { VoiceState(it, kord) } + +} \ No newline at end of file diff --git a/core/src/main/kotlin/behavior/channel/StageChannelBehavior.kt b/core/src/main/kotlin/behavior/channel/StageChannelBehavior.kt new file mode 100644 index 00000000000..2d93dcc7bd7 --- /dev/null +++ b/core/src/main/kotlin/behavior/channel/StageChannelBehavior.kt @@ -0,0 +1,87 @@ +package dev.kord.core.behavior.channel + +import dev.kord.common.entity.Snowflake +import dev.kord.core.Kord +import dev.kord.core.cache.data.ChannelData +import dev.kord.core.entity.channel.Channel +import dev.kord.core.entity.channel.StageChannel +import dev.kord.core.entity.channel.VoiceChannel +import dev.kord.core.supplier.EntitySupplier +import dev.kord.core.supplier.EntitySupplyStrategy +import dev.kord.rest.builder.channel.StageVoiceChannelModifyBuilder +import dev.kord.rest.builder.guild.CurrentVoiceStateModifyBuilder +import dev.kord.rest.builder.guild.VoiceStateModifyBuilder +import dev.kord.rest.request.RestRequestException +import dev.kord.rest.service.modifyCurrentVoiceState +import dev.kord.rest.service.modifyVoiceState +import dev.kord.rest.service.patchStageVoiceChannel +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +interface StageChannelBehavior : BaseVoiceChannelBehavior { + + /** + * Returns a new [StageChannelBehavior] with the given [strategy]. + */ + override fun withStrategy( + strategy: EntitySupplyStrategy<*> + ): StageChannelBehavior { + return StageChannelBehavior(id, guildId, kord, strategy.supply(kord)) + } + +} + +/** + * Requests to edit the current user's voice state in this [StageChannel]. + */ +@OptIn(ExperimentalContracts::class) +suspend inline fun StageChannelBehavior.editCurrentVoiceState(builder: CurrentVoiceStateModifyBuilder.() -> Unit) { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + kord.rest.guild.modifyCurrentVoiceState(guildId, id, builder) +} + +/** + * Requests to edit the another user's voice state in this [StageChannel]. + */ +@OptIn(ExperimentalContracts::class) +suspend inline fun StageChannelBehavior.editVoiceState( + userId: Snowflake, + builder: VoiceStateModifyBuilder.() -> Unit +) { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + kord.rest.guild.modifyVoiceState(guildId, id, userId, builder) +} + +/** + * Requests to edit this channel. + * + * @return The edited [StageChannel]. + * + * @throws [RestRequestException] if something went wrong during the request. + */ +@OptIn(ExperimentalContracts::class) +suspend fun StageChannelBehavior.edit(builder: StageVoiceChannelModifyBuilder.() -> Unit): StageChannel { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + val response = kord.rest.channel.patchStageVoiceChannel(id, builder) + + val data = ChannelData.from(response) + return Channel.from(data, kord) as StageChannel +} + +fun StageChannelBehavior( + id: Snowflake, + guildId: Snowflake, + kord: Kord, + supplier: EntitySupplier = kord.defaultSupplier +): StageChannelBehavior = object : StageChannelBehavior { + override val guildId: Snowflake + get() = guildId + override val kord get() = kord + override val id: Snowflake get() = id + override val supplier get() = supplier + + override fun toString(): String { + return "StageChannelBehavior(id=$id, guildId=$guildId, kord=$kord, supplier=$supplier)" + } +} diff --git a/core/src/main/kotlin/behavior/channel/VoiceChannelBehavior.kt b/core/src/main/kotlin/behavior/channel/VoiceChannelBehavior.kt index 54c29c86d22..7a052f4cb95 100644 --- a/core/src/main/kotlin/behavior/channel/VoiceChannelBehavior.kt +++ b/core/src/main/kotlin/behavior/channel/VoiceChannelBehavior.kt @@ -1,14 +1,9 @@ package dev.kord.core.behavior.channel -import dev.kord.cache.api.query - import dev.kord.common.entity.Snowflake import dev.kord.common.exception.RequestException import dev.kord.core.Kord import dev.kord.core.cache.data.ChannelData -import dev.kord.core.cache.data.VoiceStateData -import dev.kord.core.cache.idEq -import dev.kord.core.entity.VoiceState import dev.kord.core.entity.channel.Channel import dev.kord.core.entity.channel.VoiceChannel import dev.kord.core.exception.EntityNotFoundException @@ -17,8 +12,6 @@ import dev.kord.core.supplier.EntitySupplyStrategy import dev.kord.rest.builder.channel.VoiceChannelModifyBuilder import dev.kord.rest.request.RestRequestException import dev.kord.rest.service.patchVoiceChannel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map import java.util.* import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind @@ -27,20 +20,7 @@ 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 - get() = kord.cache.query { idEq(VoiceStateData::channelId, id) } - .asFlow() - .map { VoiceState(it, kord) } +interface VoiceChannelBehavior : BaseVoiceChannelBehavior { /** * Requests to get the this behavior as a [VoiceChannel]. diff --git a/core/src/main/kotlin/cache/data/ActivityData.kt b/core/src/main/kotlin/cache/data/ActivityData.kt index 5346dc820df..fbcff408dcc 100644 --- a/core/src/main/kotlin/cache/data/ActivityData.kt +++ b/core/src/main/kotlin/cache/data/ActivityData.kt @@ -22,6 +22,7 @@ data class ActivityData( val secrets: Optional = Optional.Missing(), val instance: OptionalBoolean = OptionalBoolean.Missing, val flags: Optional = Optional.Missing(), + val buttons: Optional> = Optional.Missing() ) { companion object { fun from(entity: DiscordActivity) = with(entity) { @@ -39,7 +40,8 @@ data class ActivityData( assets, secrets, instance, - flags + flags, + buttons ) } } diff --git a/core/src/main/kotlin/cache/data/GuildData.kt b/core/src/main/kotlin/cache/data/GuildData.kt index 2da5a2aa2d5..32e92ede9b9 100644 --- a/core/src/main/kotlin/cache/data/GuildData.kt +++ b/core/src/main/kotlin/cache/data/GuildData.kt @@ -54,6 +54,8 @@ data class GuildData( val maxVideoChannelUsers: OptionalInt = OptionalInt.Missing, val approximateMemberCount: OptionalInt = OptionalInt.Missing, val approximatePresenceCount: OptionalInt = OptionalInt.Missing, + val welcomeScreen: Optional = Optional.Missing(), + val nsfw: Boolean ) { companion object { @@ -113,6 +115,8 @@ data class GuildData( maxVideoChannelUsers = maxVideoChannelUsers, approximateMemberCount = approximateMemberCount, approximatePresenceCount = approximatePresenceCount, + welcomeScreen = welcomeScreen.map { WelcomeScreenData.from(it) }, + nsfw = nsfw ) } } diff --git a/core/src/main/kotlin/cache/data/MessageData.kt b/core/src/main/kotlin/cache/data/MessageData.kt index b64740e94aa..1ced0836472 100644 --- a/core/src/main/kotlin/cache/data/MessageData.kt +++ b/core/src/main/kotlin/cache/data/MessageData.kt @@ -30,6 +30,7 @@ data class MessageData( val type: MessageType, val activity: Optional = Optional.Missing(), val application: Optional = Optional.Missing(), + val applicationId: OptionalSnowflake = OptionalSnowflake.Missing, val messageReference: Optional = Optional.Missing(), val flags: Optional = Optional.Missing(), val stickers: Optional> = Optional.Missing(), @@ -92,6 +93,7 @@ data class MessageData( type, activity, application, + applicationId, messageReference, flags, stickers = stickers, @@ -124,6 +126,7 @@ data class MessageData( type, activity, application, + applicationId, messageReference.map { MessageReferenceData.from(it) }, flags, stickers.mapList { MessageStickerData.from(it) }, diff --git a/core/src/main/kotlin/cache/data/VoiceStateData.kt b/core/src/main/kotlin/cache/data/VoiceStateData.kt index 73d702d410f..5d580fa53ed 100644 --- a/core/src/main/kotlin/cache/data/VoiceStateData.kt +++ b/core/src/main/kotlin/cache/data/VoiceStateData.kt @@ -1,14 +1,11 @@ package dev.kord.core.cache.data import dev.kord.cache.api.data.description -import dev.kord.common.entity.DiscordGuildMember import dev.kord.common.entity.DiscordVoiceState import dev.kord.common.entity.Snowflake -import dev.kord.common.entity.optional.Optional import dev.kord.common.entity.optional.OptionalBoolean import dev.kord.common.entity.optional.OptionalSnowflake import dev.kord.common.entity.optional.mapSnowflake -import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable val VoiceStateData.id get() = "$userId$guildId" @@ -31,6 +28,7 @@ data class VoiceStateData( val selfMute: Boolean, val selfStream: OptionalBoolean = OptionalBoolean.Missing, val suppress: Boolean, + val requestToSpeakTimestamp: String? ) { companion object { @@ -48,7 +46,8 @@ data class VoiceStateData( selfDeaf = selfDeaf, selfMute = selfMute, selfStream = selfStream, - suppress = suppress + suppress = suppress, + requestToSpeakTimestamp = requestToSpeakTimestamp ) } } diff --git a/core/src/main/kotlin/entity/Activity.kt b/core/src/main/kotlin/entity/Activity.kt index 299c4ee316b..28343e450d4 100644 --- a/core/src/main/kotlin/entity/Activity.kt +++ b/core/src/main/kotlin/entity/Activity.kt @@ -57,6 +57,9 @@ class Activity(val data: ActivityData) { val flags: ActivityFlags? get() = data.flags.value + val buttons: List? + get() = data.buttons.value + override fun toString(): String { return "Activity(data=$data)" } diff --git a/core/src/main/kotlin/entity/Guild.kt b/core/src/main/kotlin/entity/Guild.kt index 0c0ec59c56e..294fc57e93c 100644 --- a/core/src/main/kotlin/entity/Guild.kt +++ b/core/src/main/kotlin/entity/Guild.kt @@ -332,6 +332,16 @@ class Guild( */ val maxVideoChannelUsers: Int? get() = data.maxVideoChannelUsers.value + /** + * The welcome screen of a Community guild, shown to new members, returned in an [Invite]'s guild object + */ + val welcomeScreen: WelcomeScreen? get() = data.welcomeScreen.unwrap { WelcomeScreen(it, kord) } + + /** + * True if this guild is [designated as NSFW](https://support.discord.com/hc/en-us/articles/1500005389362-NSFW-Server-Designation) + */ + val nsfw: Boolean get() = data.nsfw + /** * Requests to get the [VoiceChannel] represented by the [afkChannelId], * returns null if the [afkChannelId] isn't present or the channel itself isn't present. diff --git a/core/src/main/kotlin/entity/Message.kt b/core/src/main/kotlin/entity/Message.kt index 05d22f2f666..375ecff2d01 100644 --- a/core/src/main/kotlin/entity/Message.kt +++ b/core/src/main/kotlin/entity/Message.kt @@ -13,6 +13,7 @@ import dev.kord.core.entity.channel.Channel import dev.kord.core.entity.channel.GuildChannel import dev.kord.core.entity.channel.GuildMessageChannel import dev.kord.core.entity.channel.MessageChannel +import dev.kord.core.entity.interaction.Interaction import dev.kord.core.exception.EntityNotFoundException import dev.kord.core.supplier.EntitySupplier import dev.kord.core.supplier.EntitySupplyStrategy @@ -102,6 +103,11 @@ class Message( */ val stickers: List get() = data.stickers.orEmpty().map { MessageSticker(it, kord) } + /** + * If the message is a response to an [Interaction], this is the id of the interaction's application + */ + val applicationId: Snowflake? get() = data.applicationId.value + /** * The message being replied to. * diff --git a/core/src/main/kotlin/entity/VoiceState.kt b/core/src/main/kotlin/entity/VoiceState.kt index a8927beefdf..513e917f0f5 100644 --- a/core/src/main/kotlin/entity/VoiceState.kt +++ b/core/src/main/kotlin/entity/VoiceState.kt @@ -36,6 +36,8 @@ class VoiceState( val isSuppressed: Boolean get() = data.suppress + val requestToSpeakTimestamp: String? get() = data.requestToSpeakTimestamp + /** * Whether this user is streaming using "Go Live". */ @@ -48,7 +50,7 @@ class VoiceState( * @throws [RequestException] if anything went wrong during the request. */ @DeprecatedSinceKord("0.7.0") - @Deprecated("User getChannelOrNull instead.", ReplaceWith("getChannelOrNull"), DeprecationLevel.ERROR) + @Deprecated("Use getChannelOrNull instead.", ReplaceWith("getChannelOrNull"), DeprecationLevel.ERROR) suspend fun getChannel(): VoiceChannel? = channelId?.let { supplier.getChannelOfOrNull(it) } /** @@ -102,4 +104,4 @@ class VoiceState( return "VoiceState(data=$data, kord=$kord, supplier=$supplier)" } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/entity/channel/Channel.kt b/core/src/main/kotlin/entity/channel/Channel.kt index f2c9bc84a9b..b391501fa16 100644 --- a/core/src/main/kotlin/entity/channel/Channel.kt +++ b/core/src/main/kotlin/entity/channel/Channel.kt @@ -42,6 +42,7 @@ interface Channel : ChannelBehavior { ): Channel = when (data.type) { GuildText -> TextChannel(data, kord) DM, GroupDM -> DmChannel(data, kord) + GuildStageVoice -> StageChannel(data, kord) GuildVoice -> VoiceChannel(data, kord) GuildCategory -> Category(data, kord) GuildNews -> NewsChannel(data, kord) diff --git a/core/src/main/kotlin/entity/channel/StageVoiceChannel.kt b/core/src/main/kotlin/entity/channel/StageVoiceChannel.kt new file mode 100644 index 00000000000..e7f35962f6d --- /dev/null +++ b/core/src/main/kotlin/entity/channel/StageVoiceChannel.kt @@ -0,0 +1,54 @@ +package dev.kord.core.entity.channel + +import dev.kord.common.entity.optional.getOrThrow +import dev.kord.core.Kord +import dev.kord.core.behavior.channel.ChannelBehavior +import dev.kord.core.behavior.channel.GuildChannelBehavior +import dev.kord.core.behavior.channel.StageChannelBehavior +import dev.kord.core.cache.data.ChannelData +import dev.kord.core.supplier.EntitySupplier +import dev.kord.core.supplier.EntitySupplyStrategy +import java.util.* + +/** + * An instance of a [Discord Stage Channel](https://support.discord.com/hc/en-us/articles/1500005513722) + * associated to a community guild. + */ +class StageChannel( + override val data: ChannelData, + override val kord: Kord, + override val supplier: EntitySupplier = kord.defaultSupplier +) : CategorizableChannel, StageChannelBehavior { + + /** + * The bitrate (in bits) of this channel. + */ + val bitrate: Int get() = data.bitrate.getOrThrow() + + /** + * The user limit of the voice channel. + */ + val userLimit: Int get() = data.userLimit.getOrThrow() + + /** + * returns a new [StageChannel] with the given [strategy]. + * + * @param strategy the strategy to use for the new instance. By default [EntitySupplyStrategy.CacheWithRestFallback]. + */ + override fun withStrategy(strategy: EntitySupplyStrategy<*>): StageChannel = + StageChannel(data, kord, strategy.supply(kord)) + + override suspend fun asChannel(): StageChannel = 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 + } + + override fun toString(): String { + return "StageChannel(data=$data, kord=$kord, supplier=$supplier)" + } +} diff --git a/core/src/main/kotlin/event/channel/ChannelCreateEvent.kt b/core/src/main/kotlin/event/channel/ChannelCreateEvent.kt index 560f497dd71..c43203c0062 100644 --- a/core/src/main/kotlin/event/channel/ChannelCreateEvent.kt +++ b/core/src/main/kotlin/event/channel/ChannelCreateEvent.kt @@ -46,6 +46,13 @@ class VoiceChannelCreateEvent(override val channel: VoiceChannel, override val s } } + +class StageChannelCreateEvent(override val channel: StageChannel, override val shard: Int) : ChannelCreateEvent { + override fun toString(): String { + return "StageChannelCreateEvent(channel=$channel, shard=$shard)" + } +} + class UnknownChannelCreateEvent(override val channel: Channel, override val shard: Int) : ChannelCreateEvent { override fun toString(): String { return "UnknownChannelCreateEvent(channel=$channel, shard=$shard)" diff --git a/core/src/main/kotlin/event/channel/ChannelDeleteEvent.kt b/core/src/main/kotlin/event/channel/ChannelDeleteEvent.kt index 5345f62b75a..08b8993721b 100644 --- a/core/src/main/kotlin/event/channel/ChannelDeleteEvent.kt +++ b/core/src/main/kotlin/event/channel/ChannelDeleteEvent.kt @@ -46,6 +46,12 @@ class VoiceChannelDeleteEvent(override val channel: VoiceChannel, override val s } } +class StageChannelDeleteEvent(override val channel: StageChannel, override val shard: Int) : ChannelDeleteEvent { + override fun toString(): String { + return "StageChannelDeleteEvent(channel=$channel, shard=$shard)" + } +} + class UnknownChannelDeleteEvent(override val channel: Channel, override val shard: Int) : ChannelCreateEvent { override fun toString(): String { diff --git a/core/src/main/kotlin/event/channel/ChannelUpdateEvent.kt b/core/src/main/kotlin/event/channel/ChannelUpdateEvent.kt index bb6db1e141f..a87316e07a4 100644 --- a/core/src/main/kotlin/event/channel/ChannelUpdateEvent.kt +++ b/core/src/main/kotlin/event/channel/ChannelUpdateEvent.kt @@ -47,6 +47,13 @@ class VoiceChannelUpdateEvent(override val channel: VoiceChannel, override val s } +class StageChannelUpdateEvent(override val channel: StageChannel, override val shard: Int) : ChannelUpdateEvent { + override fun toString(): String { + return "StageChannelUpdateEvent(channel=$channel, shard=$shard)" + } +} + + class UnknownChannelUpdateEvent(override val channel: Channel, override val shard: Int) : ChannelCreateEvent { override fun toString(): String { return "UnknownChannelUpdateEvent(channel=$channel, shard=$shard)" diff --git a/core/src/main/kotlin/gateway/handler/ChannelEventHandler.kt b/core/src/main/kotlin/gateway/handler/ChannelEventHandler.kt index eed6c8bb642..c2c23010f72 100644 --- a/core/src/main/kotlin/gateway/handler/ChannelEventHandler.kt +++ b/core/src/main/kotlin/gateway/handler/ChannelEventHandler.kt @@ -43,6 +43,7 @@ internal class ChannelEventHandler( is StoreChannel -> StoreChannelCreateEvent(channel, shard) is DmChannel -> DMChannelCreateEvent(channel, shard) is TextChannel -> TextChannelCreateEvent(channel, shard) + is StageChannel -> StageChannelCreateEvent(channel, shard) is VoiceChannel -> VoiceChannelCreateEvent(channel, shard) is Category -> CategoryCreateEvent(channel, shard) else -> UnknownChannelCreateEvent(channel, shard) @@ -60,6 +61,7 @@ internal class ChannelEventHandler( is StoreChannel -> StoreChannelUpdateEvent(channel, shard) is DmChannel -> DMChannelUpdateEvent(channel, shard) is TextChannel -> TextChannelUpdateEvent(channel, shard) + is StageChannel -> StageChannelUpdateEvent(channel, shard) is VoiceChannel -> VoiceChannelUpdateEvent(channel, shard) is Category -> CategoryUpdateEvent(channel, shard) else -> UnknownChannelUpdateEvent(channel, shard) @@ -77,6 +79,7 @@ internal class ChannelEventHandler( is StoreChannel -> StoreChannelDeleteEvent(channel, shard) is DmChannel -> DMChannelDeleteEvent(channel, shard) is TextChannel -> TextChannelDeleteEvent(channel, shard) + is StageChannel -> StageChannelDeleteEvent(channel, shard) is VoiceChannel -> VoiceChannelDeleteEvent(channel, shard) is Category -> CategoryDeleteEvent(channel, shard) else -> UnknownChannelDeleteEvent(channel, shard) diff --git a/core/src/main/kotlin/gateway/handler/GuildEventHandler.kt b/core/src/main/kotlin/gateway/handler/GuildEventHandler.kt index 78c7dbb2dee..6fa5709330c 100644 --- a/core/src/main/kotlin/gateway/handler/GuildEventHandler.kt +++ b/core/src/main/kotlin/gateway/handler/GuildEventHandler.kt @@ -90,7 +90,7 @@ internal class GuildEventHandler( cache.put(data) event.guild.cache() - coreFlow.emit(GuildCreateEvent(Guild(data, kord), shard)) + coreFlow.emit(GuildUpdateEvent(Guild(data, kord), shard)) } private suspend fun handle(event: GuildDelete, shard: Int) = with(event.guild) { diff --git a/core/src/test/kotlin/performance/KordEventDropTest.kt b/core/src/test/kotlin/performance/KordEventDropTest.kt index 7656aa002e6..e2da51d37a9 100644 --- a/core/src/test/kotlin/performance/KordEventDropTest.kt +++ b/core/src/test/kotlin/performance/KordEventDropTest.kt @@ -60,33 +60,33 @@ class KordEventDropTest { val amount = 1_000 val event = GuildCreate( - DiscordGuild( - Snowflake("1337"), - "discord guild", - afkTimeout = 0, - defaultMessageNotifications = DefaultMessageNotificationLevel.AllMessages, - emojis = emptyList(), - explicitContentFilter = ExplicitContentFilter.AllMembers, - features = emptyList(), - mfaLevel = MFALevel.Elevated, - ownerId = Snowflake("123"), - preferredLocale = "en", - description = "A not really real guild", - premiumTier = PremiumTier.None, - region = "idk", - roles = emptyList(), - verificationLevel = VerificationLevel.High, - icon = null, - afkChannelId = null, - applicationId = null, - systemChannelFlags = SystemChannelFlags(0), - systemChannelId = null, - rulesChannelId = null, - vanityUrlCode = null, - banner = null, - publicUpdatesChannelId = null - ), 0 - ) + DiscordGuild( + Snowflake("1337"), + "discord guild", + afkTimeout = 0, + defaultMessageNotifications = DefaultMessageNotificationLevel.AllMessages, + emojis = emptyList(), + explicitContentFilter = ExplicitContentFilter.AllMembers, + features = emptyList(), + mfaLevel = MFALevel.Elevated, + ownerId = Snowflake("123"), + preferredLocale = "en", + description = "A not really real guild", + premiumTier = PremiumTier.None, + region = "idk", + roles = emptyList(), + verificationLevel = VerificationLevel.High, + icon = null, + afkChannelId = null, + applicationId = null, + systemChannelFlags = SystemChannelFlags(0), + systemChannelId = null, + rulesChannelId = null, + vanityUrlCode = null, + banner = null, + publicUpdatesChannelId = null, + nsfw = false + ), 0) val counter = AtomicInteger(0) val countdown = CountDownLatch(amount) diff --git a/gateway/src/main/kotlin/Command.kt b/gateway/src/main/kotlin/Command.kt index 8d3867b1fd6..90f228e991c 100644 --- a/gateway/src/main/kotlin/Command.kt +++ b/gateway/src/main/kotlin/Command.kt @@ -196,7 +196,7 @@ data class UpdateVoiceStatus( @Serializable data class UpdateStatus( val since: Long?, - val activities: List?, + val activities: List, val status: PresenceStatus, val afk: Boolean, ) : Command() diff --git a/gateway/src/main/kotlin/builder/PresenceBuilder.kt b/gateway/src/main/kotlin/builder/PresenceBuilder.kt index 2f8e2730773..bcf66338f43 100644 --- a/gateway/src/main/kotlin/builder/PresenceBuilder.kt +++ b/gateway/src/main/kotlin/builder/PresenceBuilder.kt @@ -32,7 +32,11 @@ class PresenceBuilder { game = DiscordBotActivity(name, ActivityType.Watching) } - fun toUpdateStatus(): UpdateStatus = UpdateStatus(since?.toEpochMilli(), game?.let(::listOf), status, afk) + fun competing(name: String) { + game = DiscordBotActivity(name, ActivityType.Competing) + } + + fun toUpdateStatus(): UpdateStatus = UpdateStatus(since?.toEpochMilli(), game?.let(::listOf).orEmpty(), status, afk) fun toPresence(): DiscordPresence = DiscordPresence(status, afk, since?.toEpochMilli(), game) } \ No newline at end of file diff --git a/gateway/src/test/kotlin/json/CommandTest.kt b/gateway/src/test/kotlin/json/CommandTest.kt index 25b9f8e7f87..45562e2233b 100644 --- a/gateway/src/test/kotlin/json/CommandTest.kt +++ b/gateway/src/test/kotlin/json/CommandTest.kt @@ -109,11 +109,11 @@ class CommandTest { @Test fun `UpdateState command serialization`() { val since = 1242518400L - val game = emptyList() + val activities = listOf() val status = PresenceStatus.Online val afk = false - val updateStatus = json.encodeToString(Command.Companion, UpdateStatus(since, game, status, afk)) + val updateStatus = json.encodeToString(Command.Companion, UpdateStatus(since, activities, status, afk)) val json = json.encodeToString(JsonObject.serializer(), buildJsonObject { put("op", OpCode.StatusUpdate.code) diff --git a/rest/src/main/kotlin/builder/channel/EditGuildChannelBuilder.kt b/rest/src/main/kotlin/builder/channel/EditGuildChannelBuilder.kt index be9c7c211b4..448a4c16b85 100644 --- a/rest/src/main/kotlin/builder/channel/EditGuildChannelBuilder.kt +++ b/rest/src/main/kotlin/builder/channel/EditGuildChannelBuilder.kt @@ -85,6 +85,36 @@ class VoiceChannelModifyBuilder : AuditRequestBuilder } + +@KordDsl +class StageVoiceChannelModifyBuilder : AuditRequestBuilder { + override var reason: String? = null + + private var _name: Optional = Optional.Missing() + var name: String? by ::_name.delegate() + + private var _position: OptionalInt? = OptionalInt.Missing + var position: Int? by ::_position.delegate() + + private var _topic: Optional = Optional.Missing() + var topic: String? by ::_topic.delegate() + + private var _parentId: OptionalSnowflake? = OptionalSnowflake.Missing + var parentId: Snowflake? by ::_parentId.delegate() + + private var _permissionOverwrites: Optional?> = Optional.Missing() + var permissionOverwrites: MutableSet? by ::_permissionOverwrites.delegate() + + override fun toRequest(): ChannelModifyPatchRequest = ChannelModifyPatchRequest( + name = _name, + position = _position, + parentId = _parentId, + topic = _topic, + permissionOverwrites = _permissionOverwrites + ) + +} + @KordDsl class NewsChannelModifyBuilder : AuditRequestBuilder { override var reason: String? = null diff --git a/rest/src/main/kotlin/builder/guild/VoiceStateModifyBuilder.kt b/rest/src/main/kotlin/builder/guild/VoiceStateModifyBuilder.kt new file mode 100644 index 00000000000..0b48f486ad6 --- /dev/null +++ b/rest/src/main/kotlin/builder/guild/VoiceStateModifyBuilder.kt @@ -0,0 +1,55 @@ +package dev.kord.rest.builder.guild + +import dev.kord.common.entity.Snowflake +import dev.kord.common.entity.optional.Optional +import dev.kord.common.entity.optional.OptionalBoolean +import dev.kord.common.entity.optional.delegate.delegate +import dev.kord.common.entity.optional.map +import dev.kord.rest.builder.RequestBuilder +import dev.kord.rest.json.request.CurrentVoiceStateModifyRequest +import dev.kord.rest.json.request.VoiceStateModifyRequest +import java.time.Instant + +class CurrentVoiceStateModifyBuilder(val channelId: Snowflake) : RequestBuilder { + + private var _requestToSpeakTimestamp: Optional = Optional.Missing() + + private var _suppress: OptionalBoolean = OptionalBoolean.Missing + + /** + * Sets the user's request to speak. + * The timestamp is used to sort how users appear on the moderators' request list. + * + * e.g: A client who requested to speak at 18:00, + * will appear above a client who requested to speak at 20:00 in the same timezone. + * + * * A date in the past is treated as "now" by Discord. + * * A null value removes the request to speak. + */ + var requestToSpeakTimestamp: Instant? by ::_requestToSpeakTimestamp.delegate() + + /** + * whether this user is muted by the current user. + */ + var suppress: Boolean? by ::_suppress.delegate() + + + override fun toRequest(): CurrentVoiceStateModifyRequest { + return CurrentVoiceStateModifyRequest(channelId, _suppress, _requestToSpeakTimestamp.map { it.toString() }) + } +} + + +class VoiceStateModifyBuilder(val channelId: Snowflake) : RequestBuilder { + + private var _suppress: OptionalBoolean = OptionalBoolean.Missing + + /** + * whether this user is muted by the current user. + */ + var suppress: Boolean? by ::_suppress.delegate() + + override fun toRequest(): VoiceStateModifyRequest { + return VoiceStateModifyRequest(channelId, _suppress) + } +} diff --git a/rest/src/main/kotlin/json/request/VoiceStateRequests.kt b/rest/src/main/kotlin/json/request/VoiceStateRequests.kt new file mode 100644 index 00000000000..2ac79942dba --- /dev/null +++ b/rest/src/main/kotlin/json/request/VoiceStateRequests.kt @@ -0,0 +1,24 @@ +package dev.kord.rest.json.request + +import dev.kord.common.entity.Snowflake +import dev.kord.common.entity.optional.Optional +import dev.kord.common.entity.optional.OptionalBoolean +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class CurrentVoiceStateModifyRequest( + @SerialName("channel_id") + val channelId: Snowflake, + val suppress: OptionalBoolean = OptionalBoolean.Missing, + @SerialName("request_to_speak_timestamp") + val requestToSpeakTimeStamp: Optional = Optional.Missing() +) + + +@Serializable +data class VoiceStateModifyRequest( + @SerialName("channel_id") + val channelId: Snowflake, + val suppress: OptionalBoolean = OptionalBoolean.Missing +) diff --git a/rest/src/main/kotlin/route/Route.kt b/rest/src/main/kotlin/route/Route.kt index ed4891dfa3d..25b0dc65184 100644 --- a/rest/src/main/kotlin/route/Route.kt +++ b/rest/src/main/kotlin/route/Route.kt @@ -665,6 +665,13 @@ sealed class Route( NoStrategy ) + object SelfVoiceStatePatch: + Route(HttpMethod.Patch, "/guilds/${GuildId}/voice-states/@me", NoStrategy) + + + object OthersVoiceStatePatch: + Route(HttpMethod.Patch, "/guilds/${GuildId}/voice-states/${UserId}", NoStrategy) + companion object { val baseUrl = "https://discord.com/api/$restVersion" } diff --git a/rest/src/main/kotlin/service/ChannelService.kt b/rest/src/main/kotlin/service/ChannelService.kt index 721e1a01957..e1e23f701e3 100644 --- a/rest/src/main/kotlin/service/ChannelService.kt +++ b/rest/src/main/kotlin/service/ChannelService.kt @@ -252,6 +252,18 @@ suspend inline fun ChannelService.patchVoiceChannel( return patchChannel(channelId, VoiceChannelModifyBuilder().apply(builder).toRequest()) } + +@OptIn(ExperimentalContracts::class) +suspend inline fun ChannelService.patchStageVoiceChannel( + channelId: Snowflake, + builder: StageVoiceChannelModifyBuilder.() -> Unit +): DiscordChannel { + contract { + callsInPlace(builder, InvocationKind.EXACTLY_ONCE) + } + return patchChannel(channelId, StageVoiceChannelModifyBuilder().apply(builder).toRequest()) +} + @OptIn(ExperimentalContracts::class) suspend inline fun ChannelService.patchStoreChannel( channelId: Snowflake, diff --git a/rest/src/main/kotlin/service/GuildService.kt b/rest/src/main/kotlin/service/GuildService.kt index 13a1cb2783b..ffafff1770f 100644 --- a/rest/src/main/kotlin/service/GuildService.kt +++ b/rest/src/main/kotlin/service/GuildService.kt @@ -5,10 +5,7 @@ import dev.kord.common.annotation.KordExperimental import dev.kord.common.entity.* import dev.kord.rest.builder.ban.BanCreateBuilder import dev.kord.rest.builder.channel.* -import dev.kord.rest.builder.guild.GuildCreateBuilder -import dev.kord.rest.builder.guild.GuildModifyBuilder -import dev.kord.rest.builder.guild.GuildWidgetModifyBuilder -import dev.kord.rest.builder.guild.WelcomeScreenModifyBuilder +import dev.kord.rest.builder.guild.* import dev.kord.rest.builder.integration.IntegrationModifyBuilder import dev.kord.rest.builder.member.MemberAddBuilder import dev.kord.rest.builder.member.MemberModifyBuilder @@ -387,6 +384,22 @@ class GuildService(requestHandler: RequestHandler) : RestService(requestHandler) body(GuildWelcomeScreenModifyRequest.serializer(), request) } + + suspend fun modifyCurrentVoiceState(guildId: Snowflake, request: CurrentVoiceStateModifyRequest) = + call(Route.SelfVoiceStatePatch) { + keys[Route.GuildId] = guildId + body(CurrentVoiceStateModifyRequest.serializer(), request) + } + + + suspend fun modifyVoiceState(guildId: Snowflake, userId: Snowflake, request: VoiceStateModifyRequest) = + call(Route.SelfVoiceStatePatch) { + keys[Route.GuildId] = guildId + keys[Route.UserId] = userId + body(VoiceStateModifyRequest.serializer(), request) + } + + } @OptIn(ExperimentalContracts::class) @@ -442,3 +455,28 @@ suspend inline fun GuildService.createCategory( val createBuilder = CategoryCreateBuilder(name).apply(builder) return createGuildChannel(guildId, createBuilder.toRequest(), createBuilder.reason) } + + +@OptIn(ExperimentalContracts::class) +suspend inline fun GuildService.modifyCurrentVoiceState( + guildId: Snowflake, + channelId: Snowflake, + builder: CurrentVoiceStateModifyBuilder.() -> Unit +) { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + val modifyBuilder = CurrentVoiceStateModifyBuilder(channelId).apply(builder) + modifyCurrentVoiceState(guildId, modifyBuilder.toRequest()) +} + + +@OptIn(ExperimentalContracts::class) +suspend inline fun GuildService.modifyVoiceState( + guildId: Snowflake, + channelId: Snowflake, + userId: Snowflake, + builder: VoiceStateModifyBuilder.() -> Unit +) { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + val modifyBuilder = VoiceStateModifyBuilder(channelId).apply(builder) + modifyVoiceState(guildId, userId, modifyBuilder.toRequest()) +} From 5be0951a58767509047814ca39ce0022a9a928a8 Mon Sep 17 00:00:00 2001 From: Michael Rittmeister Date: Fri, 14 May 2021 15:28:43 +0200 Subject: [PATCH 12/15] Fix broken CI (#293) --- .github/workflows/deployment-ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/deployment-ci.yml b/.github/workflows/deployment-ci.yml index d808280dca4..821194a760e 100644 --- a/.github/workflows/deployment-ci.yml +++ b/.github/workflows/deployment-ci.yml @@ -8,8 +8,6 @@ on: - '**' # We want to run this on all branch pushes tags-ignore: - '**' # We don't want this to run on tags pushes - branches: - - '**' pull_request: release: types: [published] From 81694e7a5229a342f2eac4496cc6341584b553d1 Mon Sep 17 00:00:00 2001 From: Michael Rittmeister Date: Thu, 20 May 2021 11:10:11 +0200 Subject: [PATCH 13/15] Migrate to kotlinx-datetime (#297) * Migrate API code to kotlinx-datetime * Update tests * Remove dead code * Replace iso serializing with kx.dt --- buildSrc/src/main/kotlin/Dependencies.kt | 2 ++ common/build.gradle.kts | 4 +++ common/src/main/kotlin/entity/Snowflake.kt | 14 ++++----- .../kotlin/ratelimit/BucketRateLimiter.kt | 16 +++++----- common/src/test/kotlin/FixedClock.kt | 8 +++++ .../kotlin/ratelimit/BucketRateLimiterTest.kt | 10 +++---- core/src/main/kotlin/Util.kt | 8 ++--- .../channel/GuildMessageChannelBehavior.kt | 9 +++--- .../channel/MessageChannelBehavior.kt | 5 ++-- core/src/main/kotlin/entity/Activity.kt | 2 +- core/src/main/kotlin/entity/Embed.kt | 4 +-- core/src/main/kotlin/entity/Guild.kt | 11 ++----- core/src/main/kotlin/entity/Integration.kt | 9 +++--- core/src/main/kotlin/entity/Member.kt | 7 ++--- core/src/main/kotlin/entity/Message.kt | 10 +++---- core/src/main/kotlin/entity/Template.kt | 8 ++--- .../kotlin/entity/channel/MessageChannel.kt | 4 +-- .../event/channel/ChannelPinsUpdateEvent.kt | 4 +-- .../kotlin/event/channel/TypingStartEvent.kt | 2 +- .../kotlin/event/guild/InviteCreateEvent.kt | 5 ++-- .../kotlin/event/guild/MemberUpdateEvent.kt | 2 +- .../kotlin/performance/KordEventDropTest.kt | 4 +-- .../main/kotlin/builder/PresenceBuilder.kt | 6 ++-- .../test/kotlin/gateway/DefaultGatewayTest.kt | 4 +-- .../builder/guild/VoiceStateModifyBuilder.kt | 2 +- .../kotlin/builder/message/EmbedBuilder.kt | 5 ++-- .../kotlin/json/request/MessageRequests.kt | 3 +- .../kotlin/ratelimit/AbstractRateLimiter.kt | 13 ++++----- .../ratelimit/ExclusionRequestRateLimiter.kt | 4 +-- .../ratelimit/ParallelRequestRateLimiter.kt | 4 +-- .../kotlin/ratelimit/RequestRateLimiter.kt | 3 +- rest/src/main/kotlin/request/HttpUtils.kt | 18 ++++++------ .../main/kotlin/request/KtorRequestHandler.kt | 6 ++-- .../AbstractRequestRateLimiterTest.kt | 29 +++++++++---------- .../ExclusionRequestRateLimiterTest.kt | 2 +- .../ParallelRequestRateLimiterTest.kt | 2 +- rest/src/test/kotlin/ratelimit/TestClock.kt | 12 ++++---- 37 files changed, 126 insertions(+), 135 deletions(-) create mode 100644 common/src/test/kotlin/FixedClock.kt diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index 6ae2e81f149..e3ce8bc60f4 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -4,6 +4,7 @@ object Versions { const val ktor = "1.5.3" const val kotlinxCoroutines = "1.5.0-RC" const val kotlinLogging = "2.0.4" + const val dateTime = "0.2.0" const val atomicFu = "0.16.1" const val binaryCompatibilityValidator = "0.4.0" @@ -28,6 +29,7 @@ object Dependencies { "org.jetbrains.kotlinx:kotlinx-serialization-json:${Versions.kotlinxSerialization}" const val `kotlinx-coroutines` = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.kotlinxCoroutines}" const val `kotlinx-atomicfu` = "org.jetbrains.kotlinx:atomicfu-jvm:${Versions.atomicFu}" + const val `kotlinx-datetime` = "org.jetbrains.kotlinx:kotlinx-datetime:${Versions.dateTime}" const val `kotlin-logging` = "io.github.microutils:kotlin-logging:${Versions.kotlinLogging}" diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 24d2dca3bed..88f16fa192b 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -13,6 +13,10 @@ configurations { } } +dependencies { + api(Dependencies.`kotlinx-datetime`) +} + tasks.withType { kotlinOptions { jvmTarget = Jvm.target diff --git a/common/src/main/kotlin/entity/Snowflake.kt b/common/src/main/kotlin/entity/Snowflake.kt index 37ac5fc6f62..187d6e20308 100644 --- a/common/src/main/kotlin/entity/Snowflake.kt +++ b/common/src/main/kotlin/entity/Snowflake.kt @@ -1,5 +1,6 @@ package dev.kord.common.entity +import kotlinx.datetime.Clock import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.PrimitiveKind @@ -7,10 +8,9 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder -import java.time.Instant +import kotlinx.datetime.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). @@ -28,11 +28,11 @@ class Snowflake(val value: Long) : Comparable { /** * Creates a Snowflake from a given [instant]. */ - constructor(instant: Instant) : this((instant.toEpochMilli() shl 22) - discordEpochLong) + constructor(instant: Instant) : this((instant.toEpochMilliseconds() shl 22) - discordEpochLong) val asString get() = value.toString() - val timeStamp: Instant get() = Instant.ofEpochMilli(discordEpochLong + (value shr 22)) + val timeStamp: Instant get() = Instant.fromEpochMilliseconds(discordEpochLong + (value shr 22)) val timeMark: TimeMark get() = SnowflakeMark(value shr 22) @@ -48,7 +48,7 @@ class Snowflake(val value: Long) : Comparable { companion object { private const val discordEpochLong = 1420070400000L - val discordEpochStart: Instant = Instant.ofEpochMilli(discordEpochLong) + val discordEpochStart: Instant = Instant.fromEpochMilliseconds(discordEpochLong) /** * The maximum value a Snowflake can hold. @@ -78,7 +78,5 @@ class Snowflake(val value: Long) : Comparable { private class SnowflakeMark(val epochMilliseconds: Long) : TimeMark() { - override fun elapsedNow(): Duration = - java.time.Duration.between(Instant.ofEpochMilli(epochMilliseconds), Instant.now()).toKotlinDuration() - + override fun elapsedNow(): Duration = Instant.fromEpochMilliseconds(epochMilliseconds) - Clock.System.now() } \ No newline at end of file diff --git a/common/src/main/kotlin/ratelimit/BucketRateLimiter.kt b/common/src/main/kotlin/ratelimit/BucketRateLimiter.kt index e613456b7dc..57bfcc21fde 100644 --- a/common/src/main/kotlin/ratelimit/BucketRateLimiter.kt +++ b/common/src/main/kotlin/ratelimit/BucketRateLimiter.kt @@ -2,11 +2,9 @@ package dev.kord.common.ratelimit import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import java.time.Clock +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant import kotlin.time.Duration -import kotlin.time.milliseconds -import kotlin.time.toKotlinDuration -import java.time.Duration as JavaDuration /** @@ -19,30 +17,30 @@ import java.time.Duration as JavaDuration class BucketRateLimiter( private val capacity: Int, private val refillInterval: Duration, - private val clock: Clock = Clock.systemUTC() + private val clock: Clock = Clock.System ) : RateLimiter { private val mutex = Mutex() private var count = 0 - private var nextInterval = 0L + private var nextInterval = Instant.fromEpochMilliseconds(0) 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 isNextInterval get() = nextInterval <= clock.now() private val isAtCapacity get() = count == capacity private fun resetState() { count = 0 - nextInterval = clock.millis() + refillInterval.inWholeMilliseconds + nextInterval = clock.now() + refillInterval } private suspend fun delayUntilNextInterval() { - val delay = nextInterval - clock.millis() + val delay = nextInterval - clock.now() kotlinx.coroutines.delay(delay) } diff --git a/common/src/test/kotlin/FixedClock.kt b/common/src/test/kotlin/FixedClock.kt new file mode 100644 index 00000000000..9268573652a --- /dev/null +++ b/common/src/test/kotlin/FixedClock.kt @@ -0,0 +1,8 @@ +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant + +fun Clock.Companion.fixed(instant: Instant): Clock = FixedClock(instant) + +private class FixedClock(private val instant: Instant): Clock { + override fun now(): Instant = instant +} diff --git a/common/src/test/kotlin/ratelimit/BucketRateLimiterTest.kt b/common/src/test/kotlin/ratelimit/BucketRateLimiterTest.kt index ba5407996ae..5122f538327 100644 --- a/common/src/test/kotlin/ratelimit/BucketRateLimiterTest.kt +++ b/common/src/test/kotlin/ratelimit/BucketRateLimiterTest.kt @@ -1,25 +1,25 @@ package ratelimit import dev.kord.common.ratelimit.BucketRateLimiter +import fixed import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runBlockingTest -import java.time.Clock -import java.time.Instant +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant import java.time.ZoneOffset import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.asserter import kotlin.time.Duration import kotlin.time.ExperimentalTime -import kotlin.time.milliseconds @ExperimentalTime @ExperimentalCoroutinesApi class BucketRateLimiterTest { val interval = Duration.milliseconds(1_000_000) - val instant = Instant.now() - val clock = Clock.fixed(instant, ZoneOffset.UTC) + val instant = Clock.System.now() + val clock = Clock.fixed(instant) lateinit var rateLimiter: BucketRateLimiter @BeforeTest diff --git a/core/src/main/kotlin/Util.kt b/core/src/main/kotlin/Util.kt index 72096671dd8..53216775441 100644 --- a/core/src/main/kotlin/Util.kt +++ b/core/src/main/kotlin/Util.kt @@ -21,8 +21,7 @@ import dev.kord.rest.json.JsonErrorCode import dev.kord.rest.request.RestRequestException import dev.kord.rest.route.Position import kotlinx.coroutines.flow.* -import java.time.Instant -import java.time.format.DateTimeFormatter +import kotlinx.datetime.Instant import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract @@ -38,9 +37,8 @@ internal fun Long?.toSnowflakeOrNull(): Snowflake? = when { 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) +internal fun Int.toInstant() = Instant.fromEpochMilliseconds(toLong()) +internal fun Long.toInstant() = Instant.fromEpochMilliseconds(this) @OptIn(ExperimentalContracts::class) internal inline fun catchNotFound(block: () -> T): T? { diff --git a/core/src/main/kotlin/behavior/channel/GuildMessageChannelBehavior.kt b/core/src/main/kotlin/behavior/channel/GuildMessageChannelBehavior.kt index 63e4f5cbc28..0e457946e94 100644 --- a/core/src/main/kotlin/behavior/channel/GuildMessageChannelBehavior.kt +++ b/core/src/main/kotlin/behavior/channel/GuildMessageChannelBehavior.kt @@ -16,8 +16,9 @@ import dev.kord.rest.request.RestRequestException import dev.kord.rest.service.RestClient import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow -import java.time.Duration -import java.time.Instant +import kotlinx.datetime.Clock +import kotlin.time.Duration +import kotlinx.datetime.Instant import java.util.* import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind @@ -70,11 +71,11 @@ interface GuildMessageChannelBehavior : GuildChannelBehavior, MessageChannelBeha * @throws [RestRequestException] if something went wrong during the request. */ suspend fun bulkDelete(messages: Iterable) { - val daysLimit = Instant.now() - Duration.ofDays(14) + val daysLimit = Clock.System.now() - Duration.days(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) } + val (younger, older) = messages.partition { it.timeStamp > daysLimit } younger.chunked(100).forEach { if (it.size < 2) kord.rest.channel.deleteMessage(id, it.first()) diff --git a/core/src/main/kotlin/behavior/channel/MessageChannelBehavior.kt b/core/src/main/kotlin/behavior/channel/MessageChannelBehavior.kt index 21fbb951c22..b702eaf046c 100644 --- a/core/src/main/kotlin/behavior/channel/MessageChannelBehavior.kt +++ b/core/src/main/kotlin/behavior/channel/MessageChannelBehavior.kt @@ -18,7 +18,8 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch -import java.time.Instant +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant import java.util.* import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind @@ -210,7 +211,7 @@ interface MessageChannelBehavior : ChannelBehavior, Strategizable { * @throws [RestRequestException] if something went wrong during the request. */ suspend fun typeUntil(instant: Instant) { - while (instant.isBefore(Instant.now())) { + while (instant < Clock.System.now()) { type() delay(Duration.seconds(8).inWholeMilliseconds) //bracing ourselves for some network delays } diff --git a/core/src/main/kotlin/entity/Activity.kt b/core/src/main/kotlin/entity/Activity.kt index 28343e450d4..e5f0bacdb90 100644 --- a/core/src/main/kotlin/entity/Activity.kt +++ b/core/src/main/kotlin/entity/Activity.kt @@ -5,7 +5,7 @@ import dev.kord.common.entity.* import dev.kord.common.entity.optional.value import dev.kord.core.cache.data.ActivityData import dev.kord.core.toInstant -import java.time.Instant +import kotlinx.datetime.Instant class Activity(val data: ActivityData) { diff --git a/core/src/main/kotlin/entity/Embed.kt b/core/src/main/kotlin/entity/Embed.kt index 7911fbde1d6..57f44d4b371 100644 --- a/core/src/main/kotlin/entity/Embed.kt +++ b/core/src/main/kotlin/entity/Embed.kt @@ -9,9 +9,9 @@ import dev.kord.common.entity.optional.value import dev.kord.core.Kord import dev.kord.core.KordObject import dev.kord.core.cache.data.* -import dev.kord.core.toInstant import dev.kord.rest.builder.message.EmbedBuilder -import java.time.Instant +import kotlinx.datetime.Instant +import kotlinx.datetime.toInstant internal const val embedDeprecationMessage = """ Embed types should be considered deprecated and might be removed in a future API version. diff --git a/core/src/main/kotlin/entity/Guild.kt b/core/src/main/kotlin/entity/Guild.kt index 294fc57e93c..a996240e8e1 100644 --- a/core/src/main/kotlin/entity/Guild.kt +++ b/core/src/main/kotlin/entity/Guild.kt @@ -27,8 +27,8 @@ import dev.kord.core.supplier.getChannelOfOrNull import dev.kord.rest.Image import dev.kord.rest.service.RestClient import kotlinx.coroutines.flow.first -import java.time.Instant -import java.time.format.DateTimeFormatter +import kotlinx.datetime.Instant +import kotlinx.datetime.toInstant import java.util.* /** @@ -182,12 +182,7 @@ class Guild( * The time at which this guild was joined, if present. */ val joinedTime: Instant? - get() = data.joinedAt.value?.let { - DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse( - it, - Instant::from - ) - } + get() = data.joinedAt.value?.toInstant() /** * The id of the owner. diff --git a/core/src/main/kotlin/entity/Integration.kt b/core/src/main/kotlin/entity/Integration.kt index 82b44db0256..91bd8c7d162 100644 --- a/core/src/main/kotlin/entity/Integration.kt +++ b/core/src/main/kotlin/entity/Integration.kt @@ -11,12 +11,11 @@ import dev.kord.core.cache.data.IntegrationData import dev.kord.core.exception.EntityNotFoundException import dev.kord.core.supplier.EntitySupplier import dev.kord.core.supplier.EntitySupplyStrategy -import dev.kord.core.toInstant import dev.kord.rest.builder.integration.IntegrationModifyBuilder import dev.kord.rest.request.RestRequestException -import java.time.Duration -import java.time.Instant -import java.time.temporal.ChronoUnit +import kotlin.time.Duration +import kotlinx.datetime.Instant +import kotlinx.datetime.toInstant import java.util.* import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind @@ -99,7 +98,7 @@ class Integration( * The grace period in days before expiring subscribers. */ val expireGracePeriod: Duration - get() = Duration.of(data.expireGracePeriod.toLong(), ChronoUnit.DAYS) + get() = Duration.days(data.expireGracePeriod) /** * The id of the [user][User] for this integration. diff --git a/core/src/main/kotlin/entity/Member.kt b/core/src/main/kotlin/entity/Member.kt index 6ccf25caef1..36a9744e47d 100644 --- a/core/src/main/kotlin/entity/Member.kt +++ b/core/src/main/kotlin/entity/Member.kt @@ -12,10 +12,9 @@ import dev.kord.core.cache.data.MemberData import dev.kord.core.cache.data.UserData import dev.kord.core.supplier.EntitySupplier import dev.kord.core.supplier.EntitySupplyStrategy -import dev.kord.core.toInstant import kotlinx.coroutines.flow.* -import java.time.Instant -import java.time.format.DateTimeFormatter +import kotlinx.datetime.toInstant +import kotlinx.datetime.Instant import java.util.* /** @@ -39,7 +38,7 @@ class Member( /** * When the user joined this [guild]. */ - val joinedAt: Instant get() = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(memberData.joinedAt, Instant::from) + val joinedAt: Instant get() = memberData.joinedAt.toInstant() /** * The guild-specific nickname of the user, if present. diff --git a/core/src/main/kotlin/entity/Message.kt b/core/src/main/kotlin/entity/Message.kt index 375ecff2d01..e5afb60c2c8 100644 --- a/core/src/main/kotlin/entity/Message.kt +++ b/core/src/main/kotlin/entity/Message.kt @@ -20,8 +20,8 @@ import dev.kord.core.supplier.EntitySupplyStrategy import dev.kord.core.supplier.getChannelOf import dev.kord.core.supplier.getChannelOfOrNull import kotlinx.coroutines.flow.* -import java.time.Instant -import java.time.format.DateTimeFormatter +import kotlinx.datetime.Instant +import kotlinx.datetime.toInstant import java.util.* /** @@ -70,9 +70,7 @@ class Message( * Returns null if the message was never edited. */ val editedTimestamp: Instant? - get() = data.editedTimestamp?.let { - DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(it, Instant::from) - } + get() = data.editedTimestamp?.toInstant() /** * The embedded content of this message. @@ -205,7 +203,7 @@ class Message( /** * The instant when this message was created. */ - val timestamp: Instant get() = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(data.timestamp, Instant::from) + val timestamp: Instant get() = data.timestamp.toInstant() /** * Whether this message was send using `\tts`. diff --git a/core/src/main/kotlin/entity/Template.kt b/core/src/main/kotlin/entity/Template.kt index 1135a1d3bb5..821b8641d93 100644 --- a/core/src/main/kotlin/entity/Template.kt +++ b/core/src/main/kotlin/entity/Template.kt @@ -5,8 +5,8 @@ import dev.kord.core.Kord import dev.kord.core.KordObject import dev.kord.core.behavior.TemplateBehavior import dev.kord.core.cache.data.TemplateData -import java.time.Instant -import java.time.format.DateTimeFormatter +import kotlinx.datetime.Instant +import kotlinx.datetime.toInstant class Template(val data: TemplateData, override val kord: Kord) : KordObject, TemplateBehavior { override val code: String get() = data.code @@ -21,9 +21,9 @@ class Template(val data: TemplateData, override val kord: Kord) : KordObject, Te val creator: User get() = User(data.creator, kord) - val createdAt: Instant get() = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(data.createdAt, Instant::from) + val createdAt: Instant get() = data.createdAt.toInstant() - val updatedAt: Instant get() = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(data.updatedAt, Instant::from) + val updatedAt: Instant get() = data.updatedAt.toInstant() override val guildId: Snowflake get() = data.sourceGuildId diff --git a/core/src/main/kotlin/entity/channel/MessageChannel.kt b/core/src/main/kotlin/entity/channel/MessageChannel.kt index 4e69db7179b..aecba2f988c 100644 --- a/core/src/main/kotlin/entity/channel/MessageChannel.kt +++ b/core/src/main/kotlin/entity/channel/MessageChannel.kt @@ -7,8 +7,8 @@ import dev.kord.core.behavior.MessageBehavior import dev.kord.core.behavior.channel.MessageChannelBehavior import dev.kord.core.entity.Message import dev.kord.core.supplier.EntitySupplyStrategy -import dev.kord.core.toInstant -import java.time.Instant +import kotlinx.datetime.Instant +import kotlinx.datetime.toInstant /** * An instance of a Discord channel that can use messages. diff --git a/core/src/main/kotlin/event/channel/ChannelPinsUpdateEvent.kt b/core/src/main/kotlin/event/channel/ChannelPinsUpdateEvent.kt index f19b3652f91..875b0a1e40b 100644 --- a/core/src/main/kotlin/event/channel/ChannelPinsUpdateEvent.kt +++ b/core/src/main/kotlin/event/channel/ChannelPinsUpdateEvent.kt @@ -12,8 +12,8 @@ import dev.kord.core.supplier.EntitySupplier import dev.kord.core.supplier.EntitySupplyStrategy import dev.kord.core.supplier.getChannelOf import dev.kord.core.supplier.getChannelOfOrNull -import dev.kord.core.toInstant -import java.time.Instant +import kotlinx.datetime.Instant +import kotlinx.datetime.toInstant class ChannelPinsUpdateEvent( val data: ChannelPinsUpdateEventData, diff --git a/core/src/main/kotlin/event/channel/TypingStartEvent.kt b/core/src/main/kotlin/event/channel/TypingStartEvent.kt index cb04f29a75a..d44659a2a3c 100644 --- a/core/src/main/kotlin/event/channel/TypingStartEvent.kt +++ b/core/src/main/kotlin/event/channel/TypingStartEvent.kt @@ -18,7 +18,7 @@ import dev.kord.core.supplier.EntitySupplyStrategy import dev.kord.core.supplier.getChannelOf import dev.kord.core.supplier.getChannelOfOrNull import dev.kord.core.toInstant -import java.time.Instant +import kotlinx.datetime.Instant class TypingStartEvent( val data: TypingStartEventData, diff --git a/core/src/main/kotlin/event/guild/InviteCreateEvent.kt b/core/src/main/kotlin/event/guild/InviteCreateEvent.kt index 5023577aaca..fb6329e7658 100644 --- a/core/src/main/kotlin/event/guild/InviteCreateEvent.kt +++ b/core/src/main/kotlin/event/guild/InviteCreateEvent.kt @@ -21,10 +21,9 @@ import dev.kord.core.supplier.EntitySupplier import dev.kord.core.supplier.EntitySupplyStrategy import dev.kord.core.supplier.getChannelOf import dev.kord.core.supplier.getChannelOfOrNull -import dev.kord.core.toInstant -import java.time.Instant +import kotlinx.datetime.Instant +import kotlinx.datetime.toInstant import kotlin.time.Duration -import kotlin.time.seconds /** * Sent when a new invite to a channel is created. diff --git a/core/src/main/kotlin/event/guild/MemberUpdateEvent.kt b/core/src/main/kotlin/event/guild/MemberUpdateEvent.kt index d82c6ead783..c42ce7a0811 100644 --- a/core/src/main/kotlin/event/guild/MemberUpdateEvent.kt +++ b/core/src/main/kotlin/event/guild/MemberUpdateEvent.kt @@ -9,7 +9,7 @@ import dev.kord.core.event.Event import dev.kord.core.supplier.EntitySupplier import dev.kord.core.supplier.EntitySupplyStrategy import kotlinx.coroutines.flow.Flow -import java.time.Instant +import kotlinx.datetime.Instant private const val deprecationMessage = "The full member is now available in this Event." diff --git a/core/src/test/kotlin/performance/KordEventDropTest.kt b/core/src/test/kotlin/performance/KordEventDropTest.kt index e2da51d37a9..19c1aa4dc77 100644 --- a/core/src/test/kotlin/performance/KordEventDropTest.kt +++ b/core/src/test/kotlin/performance/KordEventDropTest.kt @@ -17,7 +17,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import java.time.Clock +import kotlinx.datetime.Clock import java.util.concurrent.CountDownLatch import java.util.concurrent.atomic.AtomicInteger import kotlin.coroutines.CoroutineContext @@ -49,7 +49,7 @@ class KordEventDropTest { resources = ClientResources("token", Shards(1), HttpClient(), EntitySupplyStrategy.cache, Intents.none), cache = DataCache.none(), MasterGateway(mapOf(0 to SpammyGateway)), - RestClient(KtorRequestHandler("token", clock = Clock.systemUTC())), + RestClient(KtorRequestHandler("token", clock = Clock.System)), Snowflake("420"), MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE), Dispatchers.Default diff --git a/gateway/src/main/kotlin/builder/PresenceBuilder.kt b/gateway/src/main/kotlin/builder/PresenceBuilder.kt index bcf66338f43..9b4756036bc 100644 --- a/gateway/src/main/kotlin/builder/PresenceBuilder.kt +++ b/gateway/src/main/kotlin/builder/PresenceBuilder.kt @@ -7,7 +7,7 @@ import dev.kord.common.entity.DiscordBotActivity import dev.kord.common.entity.optional.Optional import dev.kord.gateway.DiscordPresence import dev.kord.gateway.UpdateStatus -import java.time.Instant +import kotlinx.datetime.Instant @KordDsl class PresenceBuilder { @@ -36,7 +36,7 @@ class PresenceBuilder { game = DiscordBotActivity(name, ActivityType.Competing) } - fun toUpdateStatus(): UpdateStatus = UpdateStatus(since?.toEpochMilli(), game?.let(::listOf).orEmpty(), status, afk) + fun toUpdateStatus(): UpdateStatus = UpdateStatus(since?.toEpochMilliseconds(), game?.let(::listOf).orEmpty(), status, afk) - fun toPresence(): DiscordPresence = DiscordPresence(status, afk, since?.toEpochMilli(), game) + fun toPresence(): DiscordPresence = DiscordPresence(status, afk, since?.toEpochMilliseconds(), game) } \ No newline at end of file diff --git a/gateway/src/test/kotlin/gateway/DefaultGatewayTest.kt b/gateway/src/test/kotlin/gateway/DefaultGatewayTest.kt index db73b1898ab..77409ea5dd4 100644 --- a/gateway/src/test/kotlin/gateway/DefaultGatewayTest.kt +++ b/gateway/src/test/kotlin/gateway/DefaultGatewayTest.kt @@ -20,10 +20,8 @@ import kotlinx.coroutines.flow.onEach import kotlinx.serialization.json.Json import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test -import java.time.Duration import kotlin.time.ExperimentalTime import kotlin.time.Duration as KDuration -import kotlin.time.toKotlinDuration @FlowPreview @KtorExperimentalAPI @@ -46,7 +44,7 @@ class DefaultGatewayTest { } reconnectRetry = LinearRetry(KDuration.seconds(2), KDuration.seconds(20), 10) - sendRateLimiter = BucketRateLimiter(120, Duration.ofSeconds(60).toKotlinDuration()) + sendRateLimiter = BucketRateLimiter(120, KDuration.seconds(60)) } gateway.events.filterIsInstance().flowOn(Dispatchers.Default).onEach { diff --git a/rest/src/main/kotlin/builder/guild/VoiceStateModifyBuilder.kt b/rest/src/main/kotlin/builder/guild/VoiceStateModifyBuilder.kt index 0b48f486ad6..bd22e803bff 100644 --- a/rest/src/main/kotlin/builder/guild/VoiceStateModifyBuilder.kt +++ b/rest/src/main/kotlin/builder/guild/VoiceStateModifyBuilder.kt @@ -8,7 +8,7 @@ import dev.kord.common.entity.optional.map import dev.kord.rest.builder.RequestBuilder import dev.kord.rest.json.request.CurrentVoiceStateModifyRequest import dev.kord.rest.json.request.VoiceStateModifyRequest -import java.time.Instant +import kotlinx.datetime.Instant class CurrentVoiceStateModifyBuilder(val channelId: Snowflake) : RequestBuilder { diff --git a/rest/src/main/kotlin/builder/message/EmbedBuilder.kt b/rest/src/main/kotlin/builder/message/EmbedBuilder.kt index cb854b44432..d5b1e933d44 100644 --- a/rest/src/main/kotlin/builder/message/EmbedBuilder.kt +++ b/rest/src/main/kotlin/builder/message/EmbedBuilder.kt @@ -9,8 +9,7 @@ import dev.kord.common.entity.optional.map import dev.kord.common.entity.optional.mapList import dev.kord.rest.builder.RequestBuilder import dev.kord.rest.json.request.* -import java.time.Instant -import java.time.format.DateTimeFormatter +import kotlinx.datetime.Instant /** * A builder for discord embeds. @@ -152,7 +151,7 @@ class EmbedBuilder : RequestBuilder { Optional.Value("embed"), _description, _url, - _timestamp.map { DateTimeFormatter.ISO_INSTANT.format(it) }, + _timestamp, _color, _footer.map { it.toRequest() }, _image.map { EmbedImageRequest(it) }, diff --git a/rest/src/main/kotlin/json/request/MessageRequests.kt b/rest/src/main/kotlin/json/request/MessageRequests.kt index 3016a506fa8..3621de535ff 100644 --- a/rest/src/main/kotlin/json/request/MessageRequests.kt +++ b/rest/src/main/kotlin/json/request/MessageRequests.kt @@ -5,6 +5,7 @@ import dev.kord.common.entity.* import dev.kord.common.entity.optional.Optional import dev.kord.common.entity.optional.OptionalBoolean import dev.kord.common.entity.optional.OptionalInt +import kotlinx.datetime.Instant import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -38,7 +39,7 @@ data class EmbedRequest( val type: Optional = Optional.Missing(), val description: Optional = Optional.Missing(), val url: Optional = Optional.Missing(), - val timestamp: Optional = Optional.Missing(), + val timestamp: Optional = Optional.Missing(), val color: Optional = Optional.Missing(), val footer: Optional = Optional.Missing(), val image: Optional = Optional.Missing(), diff --git a/rest/src/main/kotlin/ratelimit/AbstractRateLimiter.kt b/rest/src/main/kotlin/ratelimit/AbstractRateLimiter.kt index 41421dfc642..ba676267ad4 100644 --- a/rest/src/main/kotlin/ratelimit/AbstractRateLimiter.kt +++ b/rest/src/main/kotlin/ratelimit/AbstractRateLimiter.kt @@ -10,9 +10,8 @@ import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.delay import kotlinx.coroutines.sync.Mutex import mu.KLogger -import java.time.Clock +import kotlinx.datetime.Clock import kotlin.time.Duration as KDuration -import java.time.Duration import java.util.concurrent.ConcurrentHashMap @@ -20,7 +19,7 @@ abstract class AbstractRateLimiter internal constructor(val clock: Clock) : Requ internal abstract val logger: KLogger internal val autoBanRateLimiter = BucketRateLimiter(25000, KDuration.minutes(10)) - internal val globalSuspensionPoint = atomic(Reset(clock.instant())) + internal val globalSuspensionPoint = atomic(Reset(clock.now())) internal val buckets = ConcurrentHashMap() internal val routeBuckets = ConcurrentHashMap>() @@ -29,9 +28,9 @@ abstract class AbstractRateLimiter internal constructor(val clock: Clock) : Requ 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()) + val duration = value - clock.now() + if (duration.isNegative()) return + delay(duration) } open override suspend fun await(request: Request<*, *>): RequestToken { @@ -84,7 +83,7 @@ abstract class AbstractRateLimiter internal constructor(val clock: Clock) : Requ } internal inner class Bucket(val id: BucketKey) { - val reset = atomic(Reset(clock.instant())) + val reset = atomic(Reset(clock.now())) val mutex = Mutex() suspend fun awaitAndLock() { diff --git a/rest/src/main/kotlin/ratelimit/ExclusionRequestRateLimiter.kt b/rest/src/main/kotlin/ratelimit/ExclusionRequestRateLimiter.kt index d8b5dacf8ac..087efd7d0e4 100644 --- a/rest/src/main/kotlin/ratelimit/ExclusionRequestRateLimiter.kt +++ b/rest/src/main/kotlin/ratelimit/ExclusionRequestRateLimiter.kt @@ -6,7 +6,7 @@ import dev.kord.rest.request.identifier import kotlinx.coroutines.sync.Mutex import mu.KLogger import mu.KotlinLogging -import java.time.Clock +import kotlinx.datetime.Clock private val requestLogger = KotlinLogging.logger {} @@ -17,7 +17,7 @@ private val requestLogger = KotlinLogging.logger {} * * @param clock a [Clock] used for calculating suspension times, present for testing purposes. */ -class ExclusionRequestRateLimiter(clock: Clock = Clock.systemUTC()) : AbstractRateLimiter(clock) { +class ExclusionRequestRateLimiter(clock: Clock = Clock.System) : AbstractRateLimiter(clock) { override val logger: KLogger get() = requestLogger private val sequentialLock = Mutex() diff --git a/rest/src/main/kotlin/ratelimit/ParallelRequestRateLimiter.kt b/rest/src/main/kotlin/ratelimit/ParallelRequestRateLimiter.kt index 2774f82dfdc..e096f950ea0 100644 --- a/rest/src/main/kotlin/ratelimit/ParallelRequestRateLimiter.kt +++ b/rest/src/main/kotlin/ratelimit/ParallelRequestRateLimiter.kt @@ -6,7 +6,7 @@ import dev.kord.rest.request.RequestIdentifier import dev.kord.rest.request.identifier import mu.KLogger import mu.KotlinLogging -import java.time.Clock +import kotlinx.datetime.Clock private val parallelLogger = KotlinLogging.logger {} @@ -24,7 +24,7 @@ private val parallelLogger = KotlinLogging.logger {} * @param clock a [Clock] used for calculating suspension times, present for testing purposes. */ @KordUnsafe -class ParallelRequestRateLimiter(clock: Clock = Clock.systemUTC()) : AbstractRateLimiter(clock) { +class ParallelRequestRateLimiter(clock: Clock = Clock.System) : AbstractRateLimiter(clock) { override val logger: KLogger get() = parallelLogger diff --git a/rest/src/main/kotlin/ratelimit/RequestRateLimiter.kt b/rest/src/main/kotlin/ratelimit/RequestRateLimiter.kt index 9e7dc8a424f..0d21e3b36e0 100644 --- a/rest/src/main/kotlin/ratelimit/RequestRateLimiter.kt +++ b/rest/src/main/kotlin/ratelimit/RequestRateLimiter.kt @@ -1,8 +1,7 @@ package dev.kord.rest.ratelimit import dev.kord.rest.request.Request -import java.lang.Exception -import java.time.Instant +import kotlinx.datetime.Instant import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract diff --git a/rest/src/main/kotlin/request/HttpUtils.kt b/rest/src/main/kotlin/request/HttpUtils.kt index 4684faba40c..3a1947a4caf 100644 --- a/rest/src/main/kotlin/request/HttpUtils.kt +++ b/rest/src/main/kotlin/request/HttpUtils.kt @@ -5,9 +5,9 @@ package dev.kord.rest.request import dev.kord.rest.ratelimit.BucketKey import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.readBytes -import java.time.Clock -import java.time.Duration -import java.time.Instant +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlin.time.Duration private const val rateLimitGlobalHeader = "X-RateLimit-Global" private const val retryAfterHeader = "Retry-After" @@ -19,13 +19,13 @@ 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()) + val unixSeconds = headers[resetTimeHeader]?.toDouble() ?: return Clock.System.now() + return Instant.fromEpochMilliseconds(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 seconds = headers[rateLimitResetAfter]?.toDouble() ?: return clock.now() + return clock.now().plus(Duration.milliseconds(seconds.times(1000).toLong())) } val HttpResponse.isRateLimit get() = status.value == 429 @@ -41,8 +41,8 @@ val HttpResponse.bucket: BucketKey? get() = headers[bucketRateLimitKey]?.let { B * The unix time (in ms) when the global rate limit gets reset. */ fun HttpResponse.globalSuspensionPoint(clock: Clock): Long { - val secondsWait = headers[retryAfterHeader]?.toLong() ?: return clock.millis() - return (secondsWait * 1000) + clock.millis() + val secondsWait = headers[retryAfterHeader]?.toLong() ?: return clock.now().toEpochMilliseconds() + return (secondsWait * 1000) + clock.now().toEpochMilliseconds() } fun HttpResponse.logString(body: String) = diff --git a/rest/src/main/kotlin/request/KtorRequestHandler.kt b/rest/src/main/kotlin/request/KtorRequestHandler.kt index 6ca19fcb799..162dbf875f2 100644 --- a/rest/src/main/kotlin/request/KtorRequestHandler.kt +++ b/rest/src/main/kotlin/request/KtorRequestHandler.kt @@ -14,7 +14,7 @@ import io.ktor.http.* import io.ktor.http.content.* import kotlinx.serialization.json.Json import mu.KotlinLogging -import java.time.Clock +import kotlinx.datetime.Clock internal val jsonDefault = Json { encodeDefaults = false @@ -35,7 +35,7 @@ internal val jsonDefault = Json { class KtorRequestHandler( private val client: HttpClient, private val requestRateLimiter: RequestRateLimiter = ExclusionRequestRateLimiter(), - private val clock: Clock = Clock.systemUTC(), + private val clock: Clock = Clock.System, private val parser: Json = jsonDefault, ) : RequestHandler { private val logger = KotlinLogging.logger("[R]:[KTOR]:[${requestRateLimiter.javaClass.simpleName}]") @@ -107,7 +107,7 @@ class KtorRequestHandler( fun KtorRequestHandler( token: String, requestRateLimiter: RequestRateLimiter = ExclusionRequestRateLimiter(), - clock: Clock = Clock.systemUTC(), + clock: Clock = Clock.System, parser: Json = jsonDefault, ): KtorRequestHandler { val client = HttpClient(CIO) { diff --git a/rest/src/test/kotlin/ratelimit/AbstractRequestRateLimiterTest.kt b/rest/src/test/kotlin/ratelimit/AbstractRequestRateLimiterTest.kt index e6460977827..2bf09c07ee4 100644 --- a/rest/src/test/kotlin/ratelimit/AbstractRequestRateLimiterTest.kt +++ b/rest/src/test/kotlin/ratelimit/AbstractRequestRateLimiterTest.kt @@ -8,16 +8,13 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeoutOrNull -import java.time.Clock -import java.time.Instant -import java.time.ZoneOffset +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant import kotlin.IllegalStateException import kotlin.test.Test import kotlin.test.assertEquals import kotlin.time.Duration import kotlin.time.ExperimentalTime -import kotlin.time.seconds -import kotlin.time.toJavaDuration @ExperimentalTime @ExperimentalCoroutinesApi @@ -26,15 +23,15 @@ abstract class AbstractRequestRateLimiterTest { abstract fun newRequestRateLimiter(clock: Clock) : RequestRateLimiter private val timeout = Duration.seconds(1000) - private val instant = Instant.EPOCH + private val instant = Instant.fromEpochMilliseconds(0) private val RateLimit.Companion.exhausted get() = RateLimit(Total(5), Remaining(0)) private suspend fun RequestRateLimiter.sendRequest(clock: TestClock, guildId: Long, bucketKey: Long = guildId, rateLimit: RateLimit) { val request = JsonRequest(Route.GuildGet, mapOf(Route.GuildId to guildId.toString()), StringValues.Empty, StringValues.Empty, null) val token = await(request) when (rateLimit.isExhausted) { - true -> token.complete(RequestResponse.BucketRateLimit(BucketKey(bucketKey.toString()), rateLimit, Reset(clock.instant().plus(timeout.toJavaDuration())))) - else -> token.complete(RequestResponse.Accepted(BucketKey(bucketKey.toString()), rateLimit, Reset(clock.instant().plus(timeout.toJavaDuration())))) + true -> token.complete(RequestResponse.BucketRateLimit(BucketKey(bucketKey.toString()), rateLimit, Reset(clock.now().plus(timeout)))) + else -> token.complete(RequestResponse.Accepted(BucketKey(bucketKey.toString()), rateLimit, Reset(clock.now().plus(timeout)))) } } @@ -45,14 +42,14 @@ abstract class AbstractRequestRateLimiterTest { private suspend fun RequestToken.complete(clock: TestClock, bucketKey: Long, rateLimit: RateLimit) { when (rateLimit.isExhausted) { - true -> complete(RequestResponse.BucketRateLimit(BucketKey(bucketKey.toString()), rateLimit, Reset(clock.instant().plus(timeout.toJavaDuration())))) - else -> complete(RequestResponse.Accepted(BucketKey(bucketKey.toString()), rateLimit, Reset(clock.instant().plus(timeout.toJavaDuration())))) + true -> complete(RequestResponse.BucketRateLimit(BucketKey(bucketKey.toString()), rateLimit, Reset(clock.now().plus(timeout)))) + else -> complete(RequestResponse.Accepted(BucketKey(bucketKey.toString()), rateLimit, Reset(clock.now().plus(timeout)))) } } @Test fun `concurrent requests on the same route are handled sequentially`() = runBlockingTest { - val clock = TestClock(instant, this, ZoneOffset.UTC) + val clock = TestClock(instant, this) val rateLimiter = newRequestRateLimiter(clock) rateLimiter.sendRequest(1).complete(clock, 1, RateLimit.exhausted) //discovery @@ -69,7 +66,7 @@ abstract class AbstractRequestRateLimiterTest { @Test fun `a RequestRateLimiter will suspend for rate limited requests with the same identifier`() = runBlockingTest { - val clock = TestClock(instant, this, ZoneOffset.UTC) + val clock = TestClock(instant, this) val rateLimiter = newRequestRateLimiter(clock) rateLimiter.sendRequest(clock, 1, rateLimit = RateLimit.exhausted) @@ -80,7 +77,7 @@ abstract class AbstractRequestRateLimiterTest { @Test fun `a RequestRateLimiter will suspend for rate limited requests with the same bucket`() = runBlockingTest { - val clock = TestClock(instant, this, ZoneOffset.UTC) + val clock = TestClock(instant, this) val rateLimiter = newRequestRateLimiter(clock) rateLimiter.sendRequest(clock, 1, 1, rateLimit = RateLimit.exhausted) @@ -92,7 +89,7 @@ abstract class AbstractRequestRateLimiterTest { @Test fun `a RequestRateLimiter will not suspend for rate limited requests that don't share an identifier`() = runBlockingTest { - val clock = TestClock(instant, this, ZoneOffset.UTC) + val clock = TestClock(instant, this) val rateLimiter = newRequestRateLimiter(clock) rateLimiter.sendRequest(clock, 1, rateLimit = RateLimit.exhausted) @@ -104,7 +101,7 @@ abstract class AbstractRequestRateLimiterTest { @Test fun `an exception during the handling won't lock the handler`() = runBlockingTest { - val clock = TestClock(instant, this, ZoneOffset.UTC) + val clock = TestClock(instant, this) val rateLimiter = newRequestRateLimiter(clock) rateLimiter.sendRequest(clock, 1, rateLimit = RateLimit(Total(5), Remaining(5))) @@ -123,7 +120,7 @@ abstract class AbstractRequestRateLimiterTest { @Test fun `REGRESSION a RequestRateLimiter encountering a non 429 error response will not throw`() = runBlockingTest { - val clock = TestClock(instant, this, ZoneOffset.UTC) + val clock = TestClock(instant, this) val rateLimiter = newRequestRateLimiter(clock) rateLimiter.sendRequest(clock, 1, rateLimit = RateLimit(Total(5), Remaining(5))) //discovery diff --git a/rest/src/test/kotlin/ratelimit/ExclusionRequestRateLimiterTest.kt b/rest/src/test/kotlin/ratelimit/ExclusionRequestRateLimiterTest.kt index dc7ab179b99..861bc1c9df1 100644 --- a/rest/src/test/kotlin/ratelimit/ExclusionRequestRateLimiterTest.kt +++ b/rest/src/test/kotlin/ratelimit/ExclusionRequestRateLimiterTest.kt @@ -1,7 +1,7 @@ package dev.kord.rest.ratelimit import kotlinx.coroutines.ExperimentalCoroutinesApi -import java.time.Clock +import kotlinx.datetime.Clock import kotlin.time.ExperimentalTime @ExperimentalTime diff --git a/rest/src/test/kotlin/ratelimit/ParallelRequestRateLimiterTest.kt b/rest/src/test/kotlin/ratelimit/ParallelRequestRateLimiterTest.kt index 7af216fc304..f39e2313b5b 100644 --- a/rest/src/test/kotlin/ratelimit/ParallelRequestRateLimiterTest.kt +++ b/rest/src/test/kotlin/ratelimit/ParallelRequestRateLimiterTest.kt @@ -1,7 +1,7 @@ package dev.kord.rest.ratelimit import kotlinx.coroutines.ExperimentalCoroutinesApi -import java.time.Clock +import kotlinx.datetime.Clock import kotlin.time.ExperimentalTime @ExperimentalTime diff --git a/rest/src/test/kotlin/ratelimit/TestClock.kt b/rest/src/test/kotlin/ratelimit/TestClock.kt index d0fdd9def7a..b3808691434 100644 --- a/rest/src/test/kotlin/ratelimit/TestClock.kt +++ b/rest/src/test/kotlin/ratelimit/TestClock.kt @@ -2,13 +2,11 @@ package dev.kord.rest.ratelimit import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestCoroutineScope -import java.time.Clock -import java.time.Instant -import java.time.ZoneId +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlin.time.Duration @ExperimentalCoroutinesApi -class TestClock(val instant: Instant, val scope: TestCoroutineScope, val zoneId: ZoneId) : Clock() { - override fun getZone(): ZoneId = zoneId - override fun instant(): Instant = instant.plusMillis(scope.currentTime) - override fun withZone(zone: ZoneId): Clock = TestClock(instant, scope, zone) +class TestClock(val instant: Instant, val scope: TestCoroutineScope) : Clock { + override fun now(): Instant = instant + Duration.milliseconds(scope.currentTime) } From db556659e79cdd56de434eb4a1cab3ef8179ff9a Mon Sep 17 00:00:00 2001 From: Michael Rittmeister Date: Thu, 27 May 2021 14:24:44 +0200 Subject: [PATCH 14/15] Bump dependencies to Kotlin 1.5.10 (#305) * Bump dependencies to Kotlin 1.5.10 - Fix sample code * Update remaining dependencies --- buildSrc/src/main/kotlin/Dependencies.kt | 12 ++++++------ core/src/samples/kotlin/PingBot.kt | 4 ---- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index e3ce8bc60f4..f10c192525a 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -1,12 +1,12 @@ object Versions { - const val kotlin = "1.5.0" + const val kotlin = "1.5.10" const val kotlinxSerialization = "1.2.1" - const val ktor = "1.5.3" - const val kotlinxCoroutines = "1.5.0-RC" - const val kotlinLogging = "2.0.4" - const val dateTime = "0.2.0" + const val ktor = "1.5.4" + const val kotlinxCoroutines = "1.5.0" + const val kotlinLogging = "2.0.6" + const val dateTime = "0.2.1" const val atomicFu = "0.16.1" - const val binaryCompatibilityValidator = "0.4.0" + const val binaryCompatibilityValidator = "0.5.0" //test deps const val kotlinTest = kotlin diff --git a/core/src/samples/kotlin/PingBot.kt b/core/src/samples/kotlin/PingBot.kt index 2582b8c3b66..048abf4e1b5 100644 --- a/core/src/samples/kotlin/PingBot.kt +++ b/core/src/samples/kotlin/PingBot.kt @@ -1,8 +1,4 @@ import dev.kord.core.Kord -import dev.kord.core.behavior.channel.createStageInstance -import dev.kord.core.behavior.channel.getStageInstance -import dev.kord.core.behavior.getChannelOf -import dev.kord.core.entity.channel.StageChannel import dev.kord.core.event.message.MessageCreateEvent import dev.kord.core.on From 1f4eeb434cdc6f78b3fb8b09d8fd6c6695d5560f Mon Sep 17 00:00:00 2001 From: BartArys Date: Wed, 2 Jun 2021 11:49:47 +0200 Subject: [PATCH 15/15] Update ktor to 1.6.0 --- buildSrc/src/main/kotlin/Dependencies.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index f10c192525a..f3cf34fcf38 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -1,7 +1,7 @@ object Versions { const val kotlin = "1.5.10" const val kotlinxSerialization = "1.2.1" - const val ktor = "1.5.4" + const val ktor = "1.6.0" const val kotlinxCoroutines = "1.5.0" const val kotlinLogging = "2.0.6" const val dateTime = "0.2.1"