From 2252fda03734f484872389808cf1aab9a7d858dd Mon Sep 17 00:00:00 2001 From: Hope <34831095+HopeBaron@users.noreply.github.com> Date: Tue, 20 Apr 2021 17:51:03 +0300 Subject: [PATCH 01/43] Ci tests improvement (#264) --- .github/workflows/deployment-ci.yml | 5 ++-- CHANGELOG.md | 6 +++++ .../src/main/kotlin/behavior/GuildBehavior.kt | 8 +++---- core/src/test/kotlin/KordTest.kt | 2 ++ core/src/test/kotlin/StrategyTest.kt | 24 ++++++++----------- .../kotlin/regression/CacheMissRegression.kt | 2 +- core/src/test/kotlin/rest/RestTest.kt | 2 +- 7 files changed, 27 insertions(+), 22 deletions(-) diff --git a/.github/workflows/deployment-ci.yml b/.github/workflows/deployment-ci.yml index a7c817adcd7..44567004d1d 100644 --- a/.github/workflows/deployment-ci.yml +++ b/.github/workflows/deployment-ci.yml @@ -4,6 +4,8 @@ name: Kotlin CI on: push: + tags-ignore: + - '**' # We don't want this to run on tags pushes pull_request: release: types: [published] @@ -39,11 +41,10 @@ jobs: if: | !contains(github.event.head_commit.message, '[publish skip]') && !contains(github.event.pull_request.title, '[publish skip]') && github.event_name != 'pull_request' && github.ref != 'refs/heads/master' + needs: build env: KORD_TEST_TOKEN: ${{ secrets.KORD_TEST_TOKEN }} - BINTRAY_KEY: ${{ secrets.BINTRAY_KEY }} - BINTRAY_USER: ${{ secrets.BINTRAY_USER }} NEXUS_USER: ${{ secrets.NEXUS_USER }} NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }} ORG_GRADLE_PROJECT_signingKey: ${{ secrets.signingKey }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ed1dcb891c..651945c08e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 0.7.0-RC3 + +## Fixes + +* Unhandled missing access when trying to get vanity url with the feature disabled. #264 + # 0.7.0-RC2 ## Additions diff --git a/core/src/main/kotlin/behavior/GuildBehavior.kt b/core/src/main/kotlin/behavior/GuildBehavior.kt index fa275de9d7d..6dc3911efbf 100644 --- a/core/src/main/kotlin/behavior/GuildBehavior.kt +++ b/core/src/main/kotlin/behavior/GuildBehavior.kt @@ -494,7 +494,7 @@ interface GuildBehavior : KordEntity, Strategizable { * @throws [RestRequestException] if something went wrong during the request. */ suspend fun getVanityUrl(): String? { - val identifier = catchDiscordError(JsonErrorCode.InviteCodeInvalidOrTaken) { + val identifier = catchDiscordError(JsonErrorCode.InviteCodeInvalidOrTaken, JsonErrorCode.MissingAccess) { kord.rest.guild.getVanityInvite(id).code } ?: return null return "https://discord.gg/$identifier" @@ -935,8 +935,8 @@ suspend inline fun GuildBehavior.bulkEditSlashCommandPermissions(noinline builde } kord.slashCommands.bulkEditApplicationCommandPermissions( - kord.selfId, - id, - builder + kord.selfId, + id, + builder ) } diff --git a/core/src/test/kotlin/KordTest.kt b/core/src/test/kotlin/KordTest.kt index 9ff57104170..a82ed4423b0 100644 --- a/core/src/test/kotlin/KordTest.kt +++ b/core/src/test/kotlin/KordTest.kt @@ -5,8 +5,10 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onCompletion import org.junit.jupiter.api.Test +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable import java.util.concurrent.CountDownLatch +@EnabledIfEnvironmentVariable(named = "KORD_TEST_TOKEN", matches = ".+") internal class KordTest { @Test diff --git a/core/src/test/kotlin/StrategyTest.kt b/core/src/test/kotlin/StrategyTest.kt index 3ba98ca9c55..2175d45597b 100644 --- a/core/src/test/kotlin/StrategyTest.kt +++ b/core/src/test/kotlin/StrategyTest.kt @@ -2,17 +2,14 @@ import dev.kord.cache.api.put import dev.kord.core.Kord import dev.kord.core.supplier.EntitySupplyStrategy import kotlinx.coroutines.runBlocking -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.Disabled -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.* import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull @TestInstance(TestInstance.Lifecycle.PER_CLASS) -@Disabled +@EnabledIfEnvironmentVariable(named = "KORD_TEST_TOKEN", matches = ".+") class StrategyTest { lateinit var kord: Kord @@ -23,28 +20,27 @@ class StrategyTest { } @Test - @EnabledIfEnvironmentVariable(named = "TARGET_BRANCH", matches = "master") + @Order(1) fun `rest only`() = runBlocking { - kord.with(EntitySupplyStrategy.rest).getSelf() + val fromRest = kord.with(EntitySupplyStrategy.rest).getSelfOrNull() val inCache = kord.with(EntitySupplyStrategy.cache).getSelfOrNull() assertNull(inCache) + assertNotNull(fromRest) } @Test - @Disabled + @Order(3) fun `cache only`() = runBlocking { - val self = kord.with(EntitySupplyStrategy.rest).getSelf() - kord.cache.put(self.data) - val inCache = kord.with(EntitySupplyStrategy.cache).getSelf() - assertEquals(self, inCache) + val inCache = kord.with(EntitySupplyStrategy.cache).getSelfOrNull() + assertNotNull(inCache) } @Test - @EnabledIfEnvironmentVariable(named = "TARGET_BRANCH", matches = "master") + @Order(2) fun `cache falls back to rest`() = runBlocking { val cache = kord.with(EntitySupplyStrategy.cache) - val inCache = cache.getSelf() + val inCache = cache.getSelfOrNull() assertNull(inCache) diff --git a/core/src/test/kotlin/regression/CacheMissRegression.kt b/core/src/test/kotlin/regression/CacheMissRegression.kt index 1868d1e3df7..01a8bd89f40 100644 --- a/core/src/test/kotlin/regression/CacheMissRegression.kt +++ b/core/src/test/kotlin/regression/CacheMissRegression.kt @@ -114,7 +114,7 @@ class CrashingHandler(val client: HttpClient) : RequestHandler { } } -@EnabledIfEnvironmentVariable(named = "TARGET_BRANCH", matches = "master") +@EnabledIfEnvironmentVariable(named = "KORD_TEST_TOKEN", matches = ".+") class CacheMissingRegressions { lateinit var kord: Kord diff --git a/core/src/test/kotlin/rest/RestTest.kt b/core/src/test/kotlin/rest/RestTest.kt index 31ffe02f15f..dee1dae8500 100644 --- a/core/src/test/kotlin/rest/RestTest.kt +++ b/core/src/test/kotlin/rest/RestTest.kt @@ -33,7 +33,7 @@ fun imageBinary(path: String): Image { @TestMethodOrder(MethodOrderer.OrderAnnotation::class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) -@EnabledIfEnvironmentVariable(named = "TARGET_BRANCH", matches = "master") +@EnabledIfEnvironmentVariable(named = "KORD_TEST_TOKEN", matches = ".+") class RestServiceTest { private val publicGuildId = Snowflake(322850917248663552) From 1a5f8bbcb128de6e2b4f972f6fdccca1cdcb253b Mon Sep 17 00:00:00 2001 From: HopeBaron Date: Tue, 20 Apr 2021 19:24:46 +0300 Subject: [PATCH 02/43] allow running dokkaHtml on push --- build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 5009c38a8d1..95179e1b92d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -93,7 +93,6 @@ subprojects { tasks.dokkaHtml.configure { - onlyIf { Library.isRelease } this.outputDirectory.set(file("${project.projectDir}/dokka/kord/")) dokkaSourceSets { From 3a12255b8860580033e533e05b5e88ec64105679 Mon Sep 17 00:00:00 2001 From: Hope <34831095+HopeBaron@users.noreply.github.com> Date: Mon, 26 Apr 2021 15:32:12 +0300 Subject: [PATCH 03/43] 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 --- .../src/main/kotlin/entity/DiscordChannel.kt | 3 + common/src/main/kotlin/entity/DiscordGuild.kt | 2 + common/src/main/kotlin/entity/Permission.kt | 7 +- .../channel/BaseVoiceChannelBehavior.kt | 26 ++++++ .../behavior/channel/StageChannelBehavior.kt | 87 +++++++++++++++++++ .../behavior/channel/VoiceChannelBehavior.kt | 22 +---- .../main/kotlin/cache/data/VoiceStateData.kt | 7 +- core/src/main/kotlin/entity/VoiceState.kt | 2 + .../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 + .../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 +++++++++- 20 files changed, 377 insertions(+), 31 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/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..914c584b742 100644 --- a/common/src/main/kotlin/entity/DiscordGuild.kt +++ b/common/src/main/kotlin/entity/DiscordGuild.kt @@ -372,6 +372,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/Permission.kt b/common/src/main/kotlin/entity/Permission.kt index c23a1051763..476413015e1 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) @@ -153,7 +154,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(Permission.values.fold(EmptyBitSet()) { acc, value -> acc + value.code }) companion object { val values: Set @@ -189,6 +191,7 @@ sealed class Permission(val code: DiscordBitSet) { ManageWebhooks, ManageEmojis, UseSlashCommands, + RequestToSpeak ) } } 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/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/VoiceState.kt b/core/src/main/kotlin/entity/VoiceState.kt index a8927beefdf..bd5009b5c32 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". */ 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/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 704953ff6f5..38fb000d9e3 100644 --- a/rest/src/main/kotlin/route/Route.kt +++ b/rest/src/main/kotlin/route/Route.kt @@ -639,6 +639,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 338950632968344a0c1c06ec12be42ffb997269d Mon Sep 17 00:00:00 2001 From: HopeBaron Date: Mon, 26 Apr 2021 15:53:31 +0300 Subject: [PATCH 04/43] Fix CI triggers --- .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..821194a760e 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 pull_request: From 69f463e339d0e80398ea534a1e45dba24d2693ad Mon Sep 17 00:00:00 2001 From: Michael Rittmeister Date: Sun, 2 May 2021 07:41:11 +0200 Subject: [PATCH 05/43] Add "Competing" activity type (Fix #270) (#272) --- common/src/main/kotlin/entity/DiscordActivity.kt | 3 ++- gateway/src/main/kotlin/builder/PresenceBuilder.kt | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/common/src/main/kotlin/entity/DiscordActivity.kt b/common/src/main/kotlin/entity/DiscordActivity.kt index ea3fdecd3c6..385e333180c 100644 --- a/common/src/main/kotlin/entity/DiscordActivity.kt +++ b/common/src/main/kotlin/entity/DiscordActivity.kt @@ -136,7 +136,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/gateway/src/main/kotlin/builder/PresenceBuilder.kt b/gateway/src/main/kotlin/builder/PresenceBuilder.kt index 2f8e2730773..63d259cf8ce 100644 --- a/gateway/src/main/kotlin/builder/PresenceBuilder.kt +++ b/gateway/src/main/kotlin/builder/PresenceBuilder.kt @@ -32,6 +32,10 @@ class PresenceBuilder { game = DiscordBotActivity(name, ActivityType.Watching) } + fun competing(name: String) { + game = DiscordBotActivity(name, ActivityType.Competing) + } + fun toUpdateStatus(): UpdateStatus = UpdateStatus(since?.toEpochMilli(), game?.let(::listOf), status, afk) fun toPresence(): DiscordPresence = DiscordPresence(status, afk, since?.toEpochMilli(), game) From fb7a8f042ea7bcedd25cbb808a753f93fc0f550d Mon Sep 17 00:00:00 2001 From: Bart Arys Date: Mon, 3 May 2021 18:11:54 +0200 Subject: [PATCH 06/43] Make Updatestatus activities not-null (#274) As per Discord's documentation: https://github.com/discord/discord-api-docs/pull/2789 --- gateway/src/main/kotlin/Command.kt | 2 +- gateway/src/main/kotlin/builder/PresenceBuilder.kt | 2 +- gateway/src/test/kotlin/json/CommandTest.kt | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) 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 63d259cf8ce..bcf66338f43 100644 --- a/gateway/src/main/kotlin/builder/PresenceBuilder.kt +++ b/gateway/src/main/kotlin/builder/PresenceBuilder.kt @@ -36,7 +36,7 @@ class PresenceBuilder { game = DiscordBotActivity(name, ActivityType.Competing) } - fun toUpdateStatus(): UpdateStatus = UpdateStatus(since?.toEpochMilli(), game?.let(::listOf), status, afk) + 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 69c736f6e57..9d0775392ec 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 @@ -101,17 +102,17 @@ class CommandTest { @Test fun `UpdateState command serialization`() { val since = 1242518400L - val game = null + 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) put("d", buildJsonObject { put("since", since) - put("activities", null as String?) + put("activities", buildJsonArray { }) put("status", status.value.toLowerCase()) put("afk", afk) }) From 2bbf1edbac9e3ae221cce0b2c5e7f478e1359e89 Mon Sep 17 00:00:00 2001 From: Bart Arys Date: Thu, 6 May 2021 21:43:53 +0200 Subject: [PATCH 07/43] 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 --- common/src/main/kotlin/DiscordBitSet.kt | 4 ++-- common/src/main/kotlin/entity/Permission.kt | 3 ++- common/src/test/kotlin/json/PermissionsTest.kt | 17 +++++++++++++++-- 3 files changed, 19 insertions(+), 5 deletions(-) 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/Permission.kt b/common/src/main/kotlin/entity/Permission.kt index 476413015e1..f9cb20f3426 100644 --- a/common/src/main/kotlin/entity/Permission.kt +++ b/common/src/main/kotlin/entity/Permission.kt @@ -131,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) @@ -155,7 +156,7 @@ sealed class Permission(val code: DiscordBitSet) { object ManageEmojis : Permission(0x40000000) object UseSlashCommands : Permission(0x80000000) object RequestToSpeak : Permission(0x100000000) - object All : Permission(Permission.values.fold(EmptyBitSet()) { acc, value -> acc + value.code }) + object All : Permission(values.fold(EmptyBitSet()) { acc, value -> acc.add(value.code); acc }) companion object { val values: Set 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 { From 0f24d9bd87fb6e8536c56e9513c2664983c86f49 Mon Sep 17 00:00:00 2001 From: Noah Hendrickson Date: Wed, 12 May 2021 07:41:15 -0400 Subject: [PATCH 08/43] Update deprecated message (#280) --- core/src/main/kotlin/entity/VoiceState.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/entity/VoiceState.kt b/core/src/main/kotlin/entity/VoiceState.kt index bd5009b5c32..513e917f0f5 100644 --- a/core/src/main/kotlin/entity/VoiceState.kt +++ b/core/src/main/kotlin/entity/VoiceState.kt @@ -50,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) } /** @@ -104,4 +104,4 @@ class VoiceState( return "VoiceState(data=$data, kord=$kord, supplier=$supplier)" } -} \ No newline at end of file +} From 0b50de6946b4251f83ac1ecd673f0d9eaff677b6 Mon Sep 17 00:00:00 2001 From: Hope <34831095+HopeBaron@users.noreply.github.com> Date: Wed, 12 May 2021 14:41:42 +0300 Subject: [PATCH 09/43] Expose the creation of application commands behavior (#281) --- core/src/main/kotlin/Unsafe.kt | 17 ++++++++++ .../GlobalApplicationCommandBehavior.kt | 33 +++++++++++++++++++ 2 files changed, 50 insertions(+) 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 From 36dc03ecb463f49fbfb3018e298517840b2dfb67 Mon Sep 17 00:00:00 2001 From: Hope <34831095+HopeBaron@users.noreply.github.com> Date: Thu, 13 May 2021 11:16:49 +0300 Subject: [PATCH 10/43] Fix GuildUpdate core handling (#284) * Expose the creation of application commands behavior * Fix type of emitted event --- core/src/main/kotlin/gateway/handler/GuildEventHandler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) { From 90a654b1a313e33180bf251a5d65cd6a50b8dbac Mon Sep 17 00:00:00 2001 From: Hope <34831095+HopeBaron@users.noreply.github.com> Date: Fri, 14 May 2021 10:29:04 +0300 Subject: [PATCH 11/43] 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 --- .../src/main/kotlin/entity/DiscordMessage.kt | 75 ++++++++++++++----- 1 file changed, 55 insertions(+), 20 deletions(-) diff --git a/common/src/main/kotlin/entity/DiscordMessage.kt b/common/src/main/kotlin/entity/DiscordMessage.kt index a802889e7c8..6bdd8fc009a 100644 --- a/common/src/main/kotlin/entity/DiscordMessage.kt +++ b/common/src/main/kotlin/entity/DiscordMessage.kt @@ -719,42 +719,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) From a623bb4e42e5899cd566f01d0f61292d477b0b99 Mon Sep 17 00:00:00 2001 From: Michael Rittmeister Date: Fri, 14 May 2021 09:56:36 +0200 Subject: [PATCH 12/43] Add buttons to Activity (#287) * Add buttons to Activity * Also pass buttons in constructor --- common/src/main/kotlin/entity/DiscordActivity.kt | 3 ++- core/src/main/kotlin/cache/data/ActivityData.kt | 4 +++- core/src/main/kotlin/entity/Activity.kt | 3 +++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/common/src/main/kotlin/entity/DiscordActivity.kt b/common/src/main/kotlin/entity/DiscordActivity.kt index 385e333180c..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) { 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/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)" } From 9b7a19812cf02edae25c451caa7a64aad7ae2ef4 Mon Sep 17 00:00:00 2001 From: Michael Rittmeister Date: Fri, 14 May 2021 10:03:48 +0200 Subject: [PATCH 13/43] Add missing fields to Guild (#288) * Add missing fields to Guild - Add welcome_screen - Add nsfw * Fix failing tests * Fix another failing tests --- common/src/main/kotlin/entity/DiscordGuild.kt | 5 ++++- common/src/test/kotlin/json/GuildTest.kt | 1 + common/src/test/resources/json/guild/guild.json | 3 ++- core/src/main/kotlin/cache/data/GuildData.kt | 4 ++++ core/src/main/kotlin/entity/Guild.kt | 10 ++++++++++ core/src/test/kotlin/performance/KordEventDropTest.kt | 3 ++- 6 files changed, 23 insertions(+), 3 deletions(-) diff --git a/common/src/main/kotlin/entity/DiscordGuild.kt b/common/src/main/kotlin/entity/DiscordGuild.kt index 914c584b742..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 ) /** 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/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/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/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/test/kotlin/performance/KordEventDropTest.kt b/core/src/test/kotlin/performance/KordEventDropTest.kt index 39542aa3970..ecc60599727 100644 --- a/core/src/test/kotlin/performance/KordEventDropTest.kt +++ b/core/src/test/kotlin/performance/KordEventDropTest.kt @@ -85,7 +85,8 @@ class KordEventDropTest { rulesChannelId = null, vanityUrlCode = null, banner = null, - publicUpdatesChannelId = null + publicUpdatesChannelId = null, + nsfw = false ), 0) val counter = AtomicInteger(0) From 2fc41ee60dfd64ef1ea39341df0b40e977c93bf1 Mon Sep 17 00:00:00 2001 From: Michael Rittmeister Date: Fri, 14 May 2021 10:05:46 +0200 Subject: [PATCH 14/43] Add Message.applicationId (#289) --- common/src/main/kotlin/entity/DiscordMessage.kt | 3 +++ core/src/main/kotlin/cache/data/MessageData.kt | 3 +++ core/src/main/kotlin/entity/Message.kt | 6 ++++++ 3 files changed, 12 insertions(+) diff --git a/common/src/main/kotlin/entity/DiscordMessage.kt b/common/src/main/kotlin/entity/DiscordMessage.kt index 6bdd8fc009a..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(), 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/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. * From b77f3263079b57ad15b09218e172b36bd8663904 Mon Sep 17 00:00:00 2001 From: Hope <34831095+HopeBaron@users.noreply.github.com> Date: Sat, 15 May 2021 21:57:07 +0300 Subject: [PATCH 15/43] Message interaction (#283) * Expose the creation of application commands behavior * Add interaction message * Apply suggestions * reference the MessageInteraction in docs * Implement Strategizable for MessageInteraction * cache user from interaction message * Fix compilation errors * Fix withStrategy return type Co-authored-by: Bart Arys Co-authored-by: Bart Arys --- .../src/main/kotlin/entity/DiscordMessage.kt | 12 ++++ .../src/main/kotlin/cache/data/MessageData.kt | 10 ++- .../cache/data/MessageInteractionData.kt | 24 +++++++ core/src/main/kotlin/entity/Message.kt | 10 +++ .../entity/interaction/MessageInteraction.kt | 67 +++++++++++++++++++ .../gateway/handler/MessageEventHandler.kt | 8 +++ 6 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 core/src/main/kotlin/cache/data/MessageInteractionData.kt create mode 100644 core/src/main/kotlin/entity/interaction/MessageInteraction.kt diff --git a/common/src/main/kotlin/entity/DiscordMessage.kt b/common/src/main/kotlin/entity/DiscordMessage.kt index e8f068c8aab..9fc5d7f3a2f 100644 --- a/common/src/main/kotlin/entity/DiscordMessage.kt +++ b/common/src/main/kotlin/entity/DiscordMessage.kt @@ -1,5 +1,6 @@ package dev.kord.common.entity +import dev.kord.common.annotation.KordPreview import dev.kord.common.entity.optional.Optional import dev.kord.common.entity.optional.OptionalBoolean import dev.kord.common.entity.optional.OptionalInt @@ -102,6 +103,7 @@ data class DiscordMessage( val stickers: Optional> = Optional.Missing(), @SerialName("referenced_message") val referencedMessage: Optional = Optional.Missing(), + val interaction: Optional = Optional.Missing() ) /** @@ -242,6 +244,7 @@ data class DiscordPartialMessage( val stickers: Optional> = Optional.Missing(), @SerialName("referenced_message") val referencedMessage: Optional = Optional.Missing(), + val interaction: Optional = Optional.Missing(), ) @Serializable @@ -827,3 +830,12 @@ data class AllowedMentions( @SerialName("replied_user") val repliedUser: OptionalBoolean = OptionalBoolean.Missing ) + +@KordPreview +@Serializable +data class DiscordMessageInteraction( + val id: Snowflake, + val type: InteractionType, + val name: String, + val user: DiscordUser +) diff --git a/core/src/main/kotlin/cache/data/MessageData.kt b/core/src/main/kotlin/cache/data/MessageData.kt index 1ced0836472..c85ff39b828 100644 --- a/core/src/main/kotlin/cache/data/MessageData.kt +++ b/core/src/main/kotlin/cache/data/MessageData.kt @@ -1,5 +1,6 @@ package dev.kord.core.cache.data +import cache.data.MessageInteractionData import dev.kord.cache.api.data.description import dev.kord.common.entity.* import dev.kord.common.entity.optional.* @@ -35,6 +36,7 @@ data class MessageData( val flags: Optional = Optional.Missing(), val stickers: Optional> = Optional.Missing(), val referencedMessage: Optional = Optional.Missing(), + val interaction: Optional = Optional.Missing() ) { fun plus(selfId: Snowflake, reaction: MessageReactionAddData): MessageData { @@ -70,6 +72,9 @@ data class MessageData( partialMessage.mentionedChannels.mapList { it.id }.switchOnMissing(mentionedChannels.value.orEmpty()) .coerceToMissing() val stickers = partialMessage.stickers.mapList { MessageStickerData.from(it) }.switchOnMissing(this.stickers) + val referencedMessage = partialMessage.referencedMessage.mapNullable { it?.toData() ?: referencedMessage.value } + val interaction = + partialMessage.interaction.map { MessageInteractionData.from(it) }.switchOnMissing(interaction) return MessageData( id, @@ -97,6 +102,8 @@ data class MessageData( messageReference, flags, stickers = stickers, + referencedMessage = referencedMessage, + interaction = interaction ) } @@ -130,7 +137,8 @@ data class MessageData( messageReference.map { MessageReferenceData.from(it) }, flags, stickers.mapList { MessageStickerData.from(it) }, - referencedMessage.mapNotNull { from(it) } + referencedMessage.mapNotNull { from(it) }, + interaction.map { MessageInteractionData.from(it) } ) } } diff --git a/core/src/main/kotlin/cache/data/MessageInteractionData.kt b/core/src/main/kotlin/cache/data/MessageInteractionData.kt new file mode 100644 index 00000000000..a7882ff8dc2 --- /dev/null +++ b/core/src/main/kotlin/cache/data/MessageInteractionData.kt @@ -0,0 +1,24 @@ +package cache.data; + +import dev.kord.common.annotation.KordPreview +import dev.kord.common.entity.DiscordMessageInteraction +import dev.kord.common.entity.InteractionType +import dev.kord.common.entity.Snowflake +import dev.kord.core.cache.data.UserData +import dev.kord.core.cache.data.toData +import kotlinx.serialization.Serializable + +@KordPreview +@Serializable +data class MessageInteractionData( + val id:Snowflake, + val type:InteractionType, + val name:String, + val user: Snowflake +) { + companion object { + fun from(entity: DiscordMessageInteraction): MessageInteractionData = with(entity) { + MessageInteractionData(id, type, name, user.id) + } + } +} diff --git a/core/src/main/kotlin/entity/Message.kt b/core/src/main/kotlin/entity/Message.kt index 375ecff2d01..d7c4c28998e 100644 --- a/core/src/main/kotlin/entity/Message.kt +++ b/core/src/main/kotlin/entity/Message.kt @@ -1,7 +1,10 @@ package dev.kord.core.entity +import dev.kord.common.annotation.KordPreview import dev.kord.common.entity.MessageType import dev.kord.common.entity.Snowflake +import dev.kord.common.entity.optional.map +import dev.kord.common.entity.optional.mapNullable import dev.kord.common.entity.optional.orEmpty import dev.kord.common.exception.RequestException import dev.kord.core.Kord @@ -13,6 +16,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.MessageInteraction import dev.kord.core.entity.interaction.Interaction import dev.kord.core.exception.EntityNotFoundException import dev.kord.core.supplier.EntitySupplier @@ -180,6 +184,12 @@ class Message( */ val mentionedUserBehaviors: Set get() = data.mentions.map { UserBehavior(it, kord) }.toSet() + /** + * The [MessageInteraction] sent on this message object when it is a response to an [dev.kord.core.entity.interaction.Interaction]. + */ + @KordPreview + val interaction: MessageInteraction? get() = data.interaction.mapNullable { MessageInteraction(it, kord) }.value + /** * The [users][User] mentioned in this message. * diff --git a/core/src/main/kotlin/entity/interaction/MessageInteraction.kt b/core/src/main/kotlin/entity/interaction/MessageInteraction.kt new file mode 100644 index 00000000000..029faf32a27 --- /dev/null +++ b/core/src/main/kotlin/entity/interaction/MessageInteraction.kt @@ -0,0 +1,67 @@ +package dev.kord.core.entity.interaction + +import cache.data.MessageInteractionData +import dev.kord.common.annotation.KordPreview +import dev.kord.common.entity.InteractionType +import dev.kord.common.entity.Snowflake +import dev.kord.common.exception.RequestException +import dev.kord.core.Kord +import dev.kord.core.behavior.UserBehavior +import dev.kord.core.entity.KordEntity +import dev.kord.core.entity.Message +import dev.kord.core.entity.Strategizable +import dev.kord.core.entity.User +import dev.kord.core.exception.EntityNotFoundException +import dev.kord.core.supplier.EntitySupplier +import dev.kord.core.supplier.EntitySupplyStrategy + +/** + * An instance of [MessageInteraction](https://discord.com/developers/docs/interactions/slash-commands#messageinteraction) + * This is sent on the [Message] object when the message is a response to an [Interaction]. + */ +@KordPreview +class MessageInteraction( + val data: MessageInteractionData, + override val kord: Kord, + override val supplier: EntitySupplier = kord.defaultSupplier +) : KordEntity, Strategizable { + /** + * [id][Interaction.id] of the [Interaction] this message is responding to. + */ + override val id: Snowflake get() = data.id + + /** + * the [name][ApplicationCommand.name] of the [ApplicationCommand] that triggered this message. + */ + val name: String get() = data.name + + /** + * The [UserBehavior] of the [user][Interaction.user] who invoked the [Interaction] + */ + val user: UserBehavior get() = UserBehavior(data.id, kord) + + /** + * the [InteractionType] of the interaction [MessageInteraction]. + */ + val type: InteractionType get() = data.type + + /** + * Requests the [User] of this interaction message. + * + * @throws RequestException if something went wrong while retrieving the user. + * @throws EntityNotFoundException if the user was null. + */ + suspend fun getUser(): User = supplier.getUser(user.id) + + /** + * Requests to get the user of this interaction message, + * returns null if the [User] isn't present. + * + * @throws [RequestException] if anything went wrong during the request. + */ + suspend fun getUserOrNull(): User? = supplier.getUserOrNull(user.id) + + override fun withStrategy(strategy: EntitySupplyStrategy<*>): MessageInteraction { + return MessageInteraction(data, kord, strategy.supply(kord)) + } +} diff --git a/core/src/main/kotlin/gateway/handler/MessageEventHandler.kt b/core/src/main/kotlin/gateway/handler/MessageEventHandler.kt index dda8eb285ef..c3b73704762 100644 --- a/core/src/main/kotlin/gateway/handler/MessageEventHandler.kt +++ b/core/src/main/kotlin/gateway/handler/MessageEventHandler.kt @@ -1,5 +1,6 @@ package dev.kord.core.gateway.handler +import cache.data.MessageInteractionData import dev.kord.cache.api.DataCache import dev.kord.cache.api.put import dev.kord.cache.api.query @@ -10,6 +11,7 @@ import dev.kord.core.cache.idEq import dev.kord.core.entity.Member import dev.kord.core.entity.Message import dev.kord.core.entity.ReactionEmoji +import dev.kord.core.entity.interaction.MessageInteraction import dev.kord.core.event.message.* import dev.kord.core.gateway.MasterGateway import dev.kord.gateway.* @@ -59,6 +61,12 @@ internal class MessageEventHandler( Member(memberData, userData, kord) } else null + //cache interaction user if present. + if(interaction is Optional.Value) { + val userData = UserData.from(interaction.value!!.user) + cache.put(userData) + } + mentions.forEach { val user = UserData.from(it) cache.put(user) From 60ab2120c6a13366562157aaccf68057f571dcfc Mon Sep 17 00:00:00 2001 From: Michael Rittmeister Date: Sat, 15 May 2021 21:02:29 +0200 Subject: [PATCH 16/43] Implement Stage instances (#291) * Add low-level implementation of stage instances * Add helper functions * Add core entities and api representations * Expose creation of StageInstanceBehavior to unsafe - Revert outdated change * Final additions - Add StageInstanceBehavior.asStageInstance - Fix compiler issue - Add StageChannelBehavior.getStageInstance() * Add StageInstances to EntitySupplier.kt * Add StageInstances to EntitySupplier.kt * Fix typo * Apply requested changes --- .../kotlin/entity/DiscordStageInstance.kt | 23 ++++++++++ core/src/main/kotlin/Unsafe.kt | 5 ++- .../kotlin/behavior/StageInstanceBehavior.kt | 45 +++++++++++++++++++ .../behavior/channel/StageChannelBehavior.kt | 16 ++++++- .../kotlin/cache/data/StageInstanceData.kt | 19 ++++++++ core/src/main/kotlin/entity/StageInstance.kt | 19 ++++++++ .../exception/EntityNotFoundException.kt | 3 ++ .../kotlin/supplier/CacheEntitySupplier.kt | 2 + .../main/kotlin/supplier/EntitySupplier.kt | 7 ++- .../kotlin/supplier/FallbackEntitySupplier.kt | 2 + .../kotlin/supplier/RestEntitySupplier.kt | 7 +++ core/src/samples/kotlin/PingBot.kt | 4 ++ .../kotlin/json/request/ChannelRequests.kt | 1 + .../json/request/StageInstanceRequests.kt | 15 +++++++ rest/src/main/kotlin/route/Route.kt | 12 +++++ rest/src/main/kotlin/service/RestClient.kt | 1 + .../kotlin/service/StageInstanceService.kt | 39 ++++++++++++++++ 17 files changed, 217 insertions(+), 3 deletions(-) create mode 100644 common/src/main/kotlin/entity/DiscordStageInstance.kt create mode 100644 core/src/main/kotlin/behavior/StageInstanceBehavior.kt create mode 100644 core/src/main/kotlin/cache/data/StageInstanceData.kt create mode 100644 core/src/main/kotlin/entity/StageInstance.kt create mode 100644 rest/src/main/kotlin/json/request/StageInstanceRequests.kt create mode 100644 rest/src/main/kotlin/service/StageInstanceService.kt diff --git a/common/src/main/kotlin/entity/DiscordStageInstance.kt b/common/src/main/kotlin/entity/DiscordStageInstance.kt new file mode 100644 index 00000000000..826eb9a4f79 --- /dev/null +++ b/common/src/main/kotlin/entity/DiscordStageInstance.kt @@ -0,0 +1,23 @@ +package dev.kord.common.entity + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + + +/** + * A _Stage Instance_ holds information about a live stage. + * + * @property id The id of this Stage instance + * @property guildId The guild id of the associated Stage channel + * @property channelId The id of the associated Stage channel + * @property topic The topic of the Stage instance (1-120 characters) + */ +@Serializable +data class DiscordStageInstance( + val id: Snowflake, + @SerialName("guild_id") + val guildId: Snowflake, + @SerialName("channel_id") + val channelId: Snowflake, + val topic: String +) diff --git a/core/src/main/kotlin/Unsafe.kt b/core/src/main/kotlin/Unsafe.kt index b09c1e206ba..c949920656d 100644 --- a/core/src/main/kotlin/Unsafe.kt +++ b/core/src/main/kotlin/Unsafe.kt @@ -68,6 +68,10 @@ class Unsafe(private val kord: Kord) { fun webhook(id: Snowflake): WebhookBehavior = WebhookBehavior(id, kord) + fun stageInstance(id: Snowflake, channelId: Snowflake): StageInstanceBehavior = StageInstanceBehavior( + id, channelId, kord, kord.defaultSupplier + ) + override fun toString(): String { return "Unsafe" } @@ -87,5 +91,4 @@ class Unsafe(private val kord: Kord) { ): GlobalApplicationCommandBehavior = GlobalApplicationCommandBehavior(applicationId, commandId, service) - } \ No newline at end of file diff --git a/core/src/main/kotlin/behavior/StageInstanceBehavior.kt b/core/src/main/kotlin/behavior/StageInstanceBehavior.kt new file mode 100644 index 00000000000..bd8f4ed2c05 --- /dev/null +++ b/core/src/main/kotlin/behavior/StageInstanceBehavior.kt @@ -0,0 +1,45 @@ +package dev.kord.core.behavior + +import dev.kord.common.entity.Snowflake +import dev.kord.core.Kord +import dev.kord.core.cache.data.StageInstanceData +import dev.kord.core.entity.KordEntity +import dev.kord.core.entity.StageInstance +import dev.kord.core.entity.Strategizable +import dev.kord.core.supplier.EntitySupplier +import dev.kord.core.supplier.EntitySupplyStrategy +import dev.kord.rest.json.request.StageInstanceUpdateRequest + +interface StageInstanceBehavior : KordEntity, Strategizable { + val channelId: Snowflake + + suspend fun delete(): Unit = kord.rest.stageInstance.deleteStageInstance(channelId) + + suspend fun update(topic: String): StageInstance { + val instance = kord.rest.stageInstance.updateStageInstance(channelId, StageInstanceUpdateRequest(topic)) + val data = StageInstanceData.from(instance) + + return StageInstance(data, kord, supplier) + } + + suspend fun asStageInstance(): StageInstance = supplier.getStageInstance(channelId) + + override fun withStrategy(strategy: EntitySupplyStrategy<*>): StageInstanceBehavior = + StageInstanceBehavior(id, channelId, kord, strategy.supply(kord)) +} + +internal fun StageInstanceBehavior(id: Snowflake, channelId: Snowflake, kord: Kord, supplier: EntitySupplier) = + object : StageInstanceBehavior { + override val channelId: Snowflake + get() = channelId + override val kord: Kord + get() = kord + override val id: Snowflake + get() = id + override val supplier: EntitySupplier + get() = supplier + + override fun toString(): String { + return "StageInstanceBehavior(id=$id, channelId=$id, kord=$kord, supplier=$supplier)" + } + } diff --git a/core/src/main/kotlin/behavior/channel/StageChannelBehavior.kt b/core/src/main/kotlin/behavior/channel/StageChannelBehavior.kt index 2d93dcc7bd7..85f818b6c29 100644 --- a/core/src/main/kotlin/behavior/channel/StageChannelBehavior.kt +++ b/core/src/main/kotlin/behavior/channel/StageChannelBehavior.kt @@ -3,15 +3,17 @@ 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.cache.data.StageInstanceData +import dev.kord.core.entity.StageInstance 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.createStageInstance import dev.kord.rest.service.modifyCurrentVoiceState import dev.kord.rest.service.modifyVoiceState import dev.kord.rest.service.patchStageVoiceChannel @@ -30,6 +32,18 @@ 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) + + return StageInstance(data, kord, supplier) + } + + suspend fun getStageInstanceOrNull(): StageInstance? = supplier.getStageInstanceOrNull(id) + + suspend fun getStageInstance(): StageInstance = supplier.getStageInstance(id) + } /** diff --git a/core/src/main/kotlin/cache/data/StageInstanceData.kt b/core/src/main/kotlin/cache/data/StageInstanceData.kt new file mode 100644 index 00000000000..43e6af3ba95 --- /dev/null +++ b/core/src/main/kotlin/cache/data/StageInstanceData.kt @@ -0,0 +1,19 @@ +package dev.kord.core.cache.data + +import dev.kord.common.entity.DiscordStageInstance +import dev.kord.common.entity.Snowflake +import kotlinx.serialization.Serializable + +@Serializable +data class StageInstanceData( + val id: Snowflake, + val guildId: Snowflake, + val channelId: Snowflake, + val topic: String +) { + companion object { + fun from(stageInstance: DiscordStageInstance) = with(stageInstance) { + StageInstanceData(id, guildId, channelId, topic) + } + } +} diff --git a/core/src/main/kotlin/entity/StageInstance.kt b/core/src/main/kotlin/entity/StageInstance.kt new file mode 100644 index 00000000000..5987e4d95e0 --- /dev/null +++ b/core/src/main/kotlin/entity/StageInstance.kt @@ -0,0 +1,19 @@ +package dev.kord.core.entity + +import dev.kord.common.entity.Snowflake +import dev.kord.core.Kord +import dev.kord.core.behavior.StageInstanceBehavior +import dev.kord.core.cache.data.StageInstanceData +import dev.kord.core.supplier.EntitySupplier +import dev.kord.core.supplier.EntitySupplyStrategy + +class StageInstance(val data: StageInstanceData, override val kord: Kord, override val supplier: EntitySupplier) : StageInstanceBehavior { + override val id: Snowflake get() = data.id + val guildId: Snowflake get() = data.guildId + override val channelId: Snowflake get() = data.channelId + val topic: String get() = data.topic + + override fun withStrategy(strategy: EntitySupplyStrategy<*>): StageInstanceBehavior = + StageInstance(data, kord, strategy.supply(kord)) + +} diff --git a/core/src/main/kotlin/exception/EntityNotFoundException.kt b/core/src/main/kotlin/exception/EntityNotFoundException.kt index 4a6ca68aed2..39c45e34699 100644 --- a/core/src/main/kotlin/exception/EntityNotFoundException.kt +++ b/core/src/main/kotlin/exception/EntityNotFoundException.kt @@ -63,6 +63,9 @@ class EntityNotFoundException : Exception { fun welcomeScreenNotFound(guildId: Snowflake): Nothing = throw EntityNotFoundException("Welcome screen for guild $guildId was not found.") + fun stageInstanceNotFound(channelId: Snowflake): Nothing = + throw EntityNotFoundException("Stage instance for channel $channelId was not found") + } } \ No newline at end of file diff --git a/core/src/main/kotlin/supplier/CacheEntitySupplier.kt b/core/src/main/kotlin/supplier/CacheEntitySupplier.kt index 5f994729de5..351fd651fd3 100644 --- a/core/src/main/kotlin/supplier/CacheEntitySupplier.kt +++ b/core/src/main/kotlin/supplier/CacheEntitySupplier.kt @@ -272,6 +272,8 @@ class CacheEntitySupplier(private val kord: Kord) : EntitySupplier { }.asFlow().map { Template(it, kord) } } + override suspend fun getStageInstanceOrNull(channelId: Snowflake): StageInstance? = null + override fun toString(): String { return "CacheEntitySupplier(cache=$cache)" } diff --git a/core/src/main/kotlin/supplier/EntitySupplier.kt b/core/src/main/kotlin/supplier/EntitySupplier.kt index 38dfae09b29..f059f4da729 100644 --- a/core/src/main/kotlin/supplier/EntitySupplier.kt +++ b/core/src/main/kotlin/supplier/EntitySupplier.kt @@ -1,5 +1,6 @@ package dev.kord.core.supplier +import dev.kord.common.entity.ChannelType.Unknown import dev.kord.common.entity.Snowflake import dev.kord.common.exception.RequestException import dev.kord.core.entity.* @@ -8,7 +9,6 @@ import dev.kord.core.entity.channel.GuildChannel import dev.kord.core.entity.channel.MessageChannel import dev.kord.core.exception.EntityNotFoundException import kotlinx.coroutines.flow.Flow -import dev.kord.common.entity.ChannelType.Unknown /** * An abstraction that allows for requesting Discord entities. @@ -389,6 +389,11 @@ interface EntitySupplier { getTemplateOrNull(code) ?: EntityNotFoundException.templateNotFound(code) fun getTemplates(guildId: Snowflake): Flow