diff --git a/build.gradle.kts b/build.gradle.kts index 95179e1b92d..5acbc04134c 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 { @@ -20,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 @@ -32,7 +33,6 @@ apply(plugin = "binary-compatibility-validator") repositories { mavenCentral() - jcenter() mavenLocal() } @@ -218,7 +218,6 @@ tasks { dokkaHtmlMultiModule.configure { dependsOn(clean) outputDirectory.set(file(dokkaOutputDir)) - 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 fb9475f42d2..f3cf34fcf38 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -1,11 +1,12 @@ object Versions { - const val kotlin = "1.4.32" - const val kotlinxSerialization = "1.1.0" - const val ktor = "1.5.2" - const val kotlinxCoroutines = "1.4.2" - const val kotlinLogging = "2.0.4" - const val atomicFu = "0.15.1" - const val binaryCompatibilityValidator = "0.4.0" + const val kotlin = "1.5.10" + const val kotlinxSerialization = "1.2.1" + const val ktor = "1.6.0" + 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.5.0" //test deps const val kotlinTest = kotlin @@ -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 93413505262..88f16fa192b 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -13,11 +13,14 @@ configurations { } } +dependencies { + api(Dependencies.`kotlinx-datetime`) +} + tasks.withType { kotlinOptions { jvmTarget = Jvm.target freeCompilerArgs = listOf( - CompilerArguments.inlineClasses, CompilerArguments.coroutines, CompilerArguments.time, CompilerArguments.optIn 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 407b071a1b4..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.inMilliseconds.toLong() + 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/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/common/src/test/kotlin/ratelimit/BucketRateLimiterTest.kt b/common/src/test/kotlin/ratelimit/BucketRateLimiterTest.kt index da970531a54..5122f538327 100644 --- a/common/src/test/kotlin/ratelimit/BucketRateLimiterTest.kt +++ b/common/src/test/kotlin/ratelimit/BucketRateLimiterTest.kt @@ -1,24 +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 = 1_000_000.milliseconds - val instant = Instant.now() - val clock = Clock.fixed(instant, ZoneOffset.UTC) + val interval = Duration.milliseconds(1_000_000) + val instant = Clock.System.now() + val clock = Clock.fixed(instant) lateinit var rateLimiter: BucketRateLimiter @BeforeTest @@ -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/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/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 df3e98301f7..b702eaf046c 100644 --- a/core/src/main/kotlin/behavior/channel/MessageChannelBehavior.kt +++ b/core/src/main/kotlin/behavior/channel/MessageChannelBehavior.kt @@ -18,14 +18,15 @@ 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 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 +200,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 } } @@ -210,9 +211,9 @@ 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(8.seconds.toLongMilliseconds()) //bracing ourselves for some network delays + delay(Duration.seconds(8).inWholeMilliseconds) //bracing ourselves for some network delays } } @@ -297,7 +298,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/behavior/channel/StageChannelBehavior.kt b/core/src/main/kotlin/behavior/channel/StageChannelBehavior.kt index 85f818b6c29..6a01603a989 100644 --- a/core/src/main/kotlin/behavior/channel/StageChannelBehavior.kt +++ b/core/src/main/kotlin/behavior/channel/StageChannelBehavior.kt @@ -3,6 +3,7 @@ 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.VoiceChannel import dev.kord.core.cache.data.StageInstanceData import dev.kord.core.entity.StageInstance import dev.kord.core.entity.channel.Channel @@ -32,7 +33,6 @@ interface StageChannelBehavior : BaseVoiceChannelBehavior { return StageChannelBehavior(id, guildId, kord, strategy.supply(kord)) } - suspend fun createStageInstance(topic: String): StageInstance { val instance = kord.rest.stageInstance.createStageInstance(id, topic) val data = StageInstanceData.from(instance) 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/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 e535f3e6a2f..8c4520d0836 100644 --- a/core/src/main/kotlin/entity/Message.kt +++ b/core/src/main/kotlin/entity/Message.kt @@ -25,8 +25,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.* /** @@ -75,9 +75,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. @@ -216,7 +214,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 9d333f007c1..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. @@ -87,7 +86,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/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/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/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/core/src/test/kotlin/performance/KordEventDropTest.kt b/core/src/test/kotlin/performance/KordEventDropTest.kt index ecc60599727..19c1aa4dc77 100644 --- a/core/src/test/kotlin/performance/KordEventDropTest.kt +++ b/core/src/test/kotlin/performance/KordEventDropTest.kt @@ -14,10 +14,10 @@ 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 java.time.Clock +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.datetime.Clock import java.util.concurrent.CountDownLatch import java.util.concurrent.atomic.AtomicInteger import kotlin.coroutines.CoroutineContext @@ -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.System)), + Snowflake("420"), + MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE), + Dispatchers.Default ) @Test @@ -100,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/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/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/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/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() } 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/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/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/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 2f1476500c4..77409ea5dd4 100644 --- a/gateway/src/test/kotlin/gateway/DefaultGatewayTest.kt +++ b/gateway/src/test/kotlin/gateway/DefaultGatewayTest.kt @@ -20,16 +20,15 @@ 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.seconds -import kotlin.time.toKotlinDuration +import kotlin.time.Duration as KDuration @FlowPreview @KtorExperimentalAPI @ObsoleteCoroutinesApi @ExperimentalCoroutinesApi class DefaultGatewayTest { + @OptIn(DelicateCoroutinesApi::class) @Test @Disabled @ExperimentalTime @@ -44,8 +43,8 @@ class DefaultGatewayTest { } } - reconnectRetry = LinearRetry(2.seconds, 20.seconds, 10) - sendRateLimiter = BucketRateLimiter(120, Duration.ofSeconds(60).toKotlinDuration()) + reconnectRetry = LinearRetry(KDuration.seconds(2), KDuration.seconds(20), 10) + sendRateLimiter = BucketRateLimiter(120, KDuration.seconds(60)) } gateway.events.filterIsInstance().flowOn(Dispatchers.Default).onEach { @@ -57,7 +56,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/gateway/src/test/kotlin/json/CommandTest.kt b/gateway/src/test/kotlin/json/CommandTest.kt index 9d0775392ec..45562e2233b 100644 --- a/gateway/src/test/kotlin/json/CommandTest.kt +++ b/gateway/src/test/kotlin/json/CommandTest.kt @@ -14,6 +14,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 } @@ -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) @@ -112,8 +119,8 @@ class CommandTest { put("op", OpCode.StatusUpdate.code) put("d", buildJsonObject { put("since", since) - put("activities", buildJsonArray { }) - put("status", status.value.toLowerCase()) + 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/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/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/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/json/request/MessageRequests.kt b/rest/src/main/kotlin/json/request/MessageRequests.kt index feaa02b9baa..6b6edc1525e 100644 --- a/rest/src/main/kotlin/json/request/MessageRequests.kt +++ b/rest/src/main/kotlin/json/request/MessageRequests.kt @@ -4,6 +4,9 @@ import dev.kord.common.Color 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 diff --git a/rest/src/main/kotlin/ratelimit/AbstractRateLimiter.kt b/rest/src/main/kotlin/ratelimit/AbstractRateLimiter.kt index 4e612be6b4f..ba676267ad4 100644 --- a/rest/src/main/kotlin/ratelimit/AbstractRateLimiter.kt +++ b/rest/src/main/kotlin/ratelimit/AbstractRateLimiter.kt @@ -10,17 +10,16 @@ import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.delay import kotlinx.coroutines.sync.Mutex import mu.KLogger -import java.time.Clock -import java.time.Duration +import kotlinx.datetime.Clock +import kotlin.time.Duration as KDuration import java.util.concurrent.ConcurrentHashMap -import kotlin.time.minutes abstract class AbstractRateLimiter internal constructor(val clock: Clock) : RequestRateLimiter { internal abstract val logger: KLogger - internal val autoBanRateLimiter = BucketRateLimiter(25000, 10.minutes) - internal val globalSuspensionPoint = atomic(Reset(clock.instant())) + internal val autoBanRateLimiter = BucketRateLimiter(25000, KDuration.minutes(10)) + 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 { @@ -45,7 +44,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,33 +54,36 @@ 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) { - 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 1a0d1331622..087efd7d0e4 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 +import kotlinx.datetime.Clock private val requestLogger = KotlinLogging.logger {} @@ -20,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() @@ -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..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,15 +24,15 @@ 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 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..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 @@ -54,26 +53,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/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 a3d9643bd37..162dbf875f2 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.* @@ -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}]") @@ -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) } } } @@ -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/main/kotlin/route/Route.kt b/rest/src/main/kotlin/route/Route.kt index bf51c0d3653..0cd5858b360 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( diff --git a/rest/src/test/kotlin/ratelimit/AbstractRequestRateLimiterTest.kt b/rest/src/test/kotlin/ratelimit/AbstractRequestRateLimiterTest.kt index 8d860a982ba..2bf09c07ee4 100644 --- a/rest/src/test/kotlin/ratelimit/AbstractRequestRateLimiterTest.kt +++ b/rest/src/test/kotlin/ratelimit/AbstractRequestRateLimiterTest.kt @@ -8,15 +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 @@ -24,16 +22,16 @@ abstract class AbstractRequestRateLimiterTest { abstract fun newRequestRateLimiter(clock: Clock) : RequestRateLimiter - private val timeout = 1000.seconds - private val instant = Instant.EPOCH + private val timeout = Duration.seconds(1000) + 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)))) } } @@ -44,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 @@ -68,30 +66,30 @@ 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) rateLimiter.sendRequest(clock, 1, rateLimit = RateLimit(Total(5), Remaining(5))) - assertEquals(timeout.inMilliseconds.toLong(), currentTime) + assertEquals(timeout.inWholeMilliseconds, currentTime) } @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) 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 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) @@ -103,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))) @@ -122,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) } 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() }