diff --git a/common/src/main/kotlin/entity/DiscordChannel.kt b/common/src/main/kotlin/entity/DiscordChannel.kt index a4aace7afa6..bcdefdb7809 100644 --- a/common/src/main/kotlin/entity/DiscordChannel.kt +++ b/common/src/main/kotlin/entity/DiscordChannel.kt @@ -93,6 +93,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 { @@ -107,6 +109,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 205a7bb4f42..ad35284a385 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 e2c3c2ed952..63a24dc5ac7 100644 --- a/common/src/main/kotlin/entity/Permission.kt +++ b/common/src/main/kotlin/entity/Permission.kt @@ -151,5 +151,6 @@ sealed class Permission(val code: DiscordBitSet) { object ManageWebhooks : Permission(0x20000000) object ManageEmojis : Permission(0x40000000) object UseSlashCommands : Permission(0x80000000) - object All : Permission(0x7FFFFDFF) + object RequestToSpeak: Permission(0x100000000) + object All : Permission(0x17FFFFDFF) } diff --git a/core/src/main/kotlin/behavior/GuildBehavior.kt b/core/src/main/kotlin/behavior/GuildBehavior.kt index de652740a4c..706aeec8036 100644 --- a/core/src/main/kotlin/behavior/GuildBehavior.kt +++ b/core/src/main/kotlin/behavior/GuildBehavior.kt @@ -33,10 +33,7 @@ import dev.kord.rest.Image import dev.kord.rest.builder.auditlog.AuditLogGetRequestBuilder import dev.kord.rest.builder.ban.BanCreateBuilder import dev.kord.rest.builder.channel.* -import dev.kord.rest.builder.guild.EmojiCreateBuilder -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.interaction.ApplicationCommandCreateBuilder import dev.kord.rest.builder.interaction.ApplicationCommandsCreateBuilder import dev.kord.rest.builder.role.RoleCreateBuilder @@ -509,7 +506,7 @@ interface GuildBehavior : KordEntity, Strategizable { override fun withStrategy(strategy: EntitySupplyStrategy<*>): GuildBehavior = GuildBehavior(id, kord, strategy) } - fun GuildBehavior( +fun GuildBehavior( id: Snowflake, kord: Kord, strategy: EntitySupplyStrategy<*> = kord.resources.defaultStrategy, diff --git a/core/src/main/kotlin/behavior/VoiceStageChannelBehavior.kt b/core/src/main/kotlin/behavior/VoiceStageChannelBehavior.kt new file mode 100644 index 00000000000..58acc537500 --- /dev/null +++ b/core/src/main/kotlin/behavior/VoiceStageChannelBehavior.kt @@ -0,0 +1,44 @@ +package dev.kord.core.behavior + +import dev.kord.common.entity.Snowflake +import dev.kord.core.Kord +import dev.kord.core.cache.data.ChannelData +import dev.kord.core.entity.channel.CategorizableChannel +import dev.kord.core.supplier.EntitySupplier +import dev.kord.core.supplier.EntitySupplyStrategy +import dev.kord.rest.builder.guild.CurrentVoiceStateModifyBuilder +import dev.kord.rest.builder.guild.VoiceStateModifyBuilder +import dev.kord.rest.service.modifyCurrentVoiceState +import dev.kord.rest.service.modifyVoiceState +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +interface VoiceStageChannelBehavior : CategorizableChannel { + + override fun withStrategy(strategy: EntitySupplyStrategy<*>): VoiceStageChannelBehavior { + return VoiceStageChannelBehavior(data, kord, strategy.supply(kord)) + } + + fun VoiceStageChannelBehavior( + data: ChannelData, + kord: Kord, + supplier: EntitySupplier = kord.defaultSupplier + ): VoiceStageChannelBehavior = object : VoiceStageChannelBehavior { + override val data: ChannelData = data + override val kord = kord + override val supplier = supplier + } +} + +@OptIn(ExperimentalContracts::class) +suspend inline fun VoiceStageChannelBehavior.editCurrentVoiceState(builder: CurrentVoiceStateModifyBuilder.() -> Unit) { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + kord.rest.guild.modifyCurrentVoiceState(guildId, id, builder) +} + +@OptIn(ExperimentalContracts::class) +suspend inline fun VoiceStageChannelBehavior.editVoiceState(userId: Snowflake, builder: VoiceStateModifyBuilder.() -> Unit) { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + kord.rest.guild.modifyVoiceState(guildId, userId, builder) +} \ No newline at end of file diff --git a/core/src/main/kotlin/cache/data/VoiceStateData.kt b/core/src/main/kotlin/cache/data/VoiceStateData.kt index 73d702d410f..3a23f52ee57 100644 --- a/core/src/main/kotlin/cache/data/VoiceStateData.kt +++ b/core/src/main/kotlin/cache/data/VoiceStateData.kt @@ -31,6 +31,7 @@ data class VoiceStateData( val selfMute: Boolean, val selfStream: OptionalBoolean = OptionalBoolean.Missing, val suppress: Boolean, + val requestToSpeakTimestamp: String? ) { companion object { @@ -48,7 +49,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 031016d30c5..46e1878baf8 100644 --- a/core/src/main/kotlin/entity/VoiceState.kt +++ b/core/src/main/kotlin/entity/VoiceState.kt @@ -41,6 +41,8 @@ class VoiceState( */ val isSelfSteaming: Boolean get() = data.selfStream.orElse(false) + val requestToSpeakTimestamp: String? get() = data.requestToSpeakTimestamp + /** * Requests to get the voice channel of this voice state. * Returns null if the [VoiceChannel] isn't present, or [channelId] is null. diff --git a/core/src/main/kotlin/entity/channel/StageChannel.kt b/core/src/main/kotlin/entity/channel/StageChannel.kt new file mode 100644 index 00000000000..bf4c82fd587 --- /dev/null +++ b/core/src/main/kotlin/entity/channel/StageChannel.kt @@ -0,0 +1,36 @@ +package dev.kord.core.entity.channel + +import dev.kord.common.entity.Snowflake +import dev.kord.common.entity.optional.getOrThrow +import dev.kord.core.Kord +import dev.kord.core.behavior.VoiceStageChannelBehavior +import dev.kord.core.cache.data.ChannelData +import dev.kord.core.supplier.EntitySupplier +import dev.kord.core.supplier.EntitySupplyStrategy + +class VoiceStageChannel( + override val data: ChannelData, + override val kord: Kord, + override val supplier: EntitySupplier = kord.defaultSupplier +) : VoiceStageChannelBehavior { + + override val id: Snowflake get() = data.id + + override val guildId: Snowflake get() = data.guildId.value!! + override fun withStrategy(strategy: EntitySupplyStrategy<*>): VoiceStageChannelBehavior { + return VoiceStageChannelBehavior(data, kord, strategy.supply(kord)) + } + + + /** + * 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() + + val topic: String get() = data.topic.value!! +} \ No newline at end of file diff --git a/rest/src/main/kotlin/builder/guild/VoiceStatePatchBuilder.kt b/rest/src/main/kotlin/builder/guild/VoiceStatePatchBuilder.kt new file mode 100644 index 00000000000..852693d08c7 --- /dev/null +++ b/rest/src/main/kotlin/builder/guild/VoiceStatePatchBuilder.kt @@ -0,0 +1,38 @@ +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.rest.builder.RequestBuilder +import dev.kord.rest.json.request.CurrentVoiceStateModifyRequest +import dev.kord.rest.json.request.VoiceStateModifyRequest + +class CurrentVoiceStateModifyBuilder(val channelId: Snowflake) : RequestBuilder { + + private var _requestToSpeakTimestamp: Optional = Optional.Missing() + + private var _suppress: OptionalBoolean = OptionalBoolean.Missing + + var requestToSpeakTimestamp: String? by ::_requestToSpeakTimestamp.delegate() + + var suppress: Boolean? by ::_suppress.delegate() + + + override fun toRequest(): CurrentVoiceStateModifyRequest { + return CurrentVoiceStateModifyRequest(channelId, _suppress, _requestToSpeakTimestamp) + } +} + + +class VoiceStateModifyBuilder(val channelId: Snowflake) : RequestBuilder { + + private var _suppress: OptionalBoolean = OptionalBoolean.Missing + + 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..0cdb775ed08 --- /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 +) \ No newline at end of file diff --git a/rest/src/main/kotlin/route/Route.kt b/rest/src/main/kotlin/route/Route.kt index 04ab7db7202..201b449c856 100644 --- a/rest/src/main/kotlin/route/Route.kt +++ b/rest/src/main/kotlin/route/Route.kt @@ -5,7 +5,6 @@ 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.request.MessageEditPatchRequest import dev.kord.rest.json.response.* import io.ktor.http.* import kotlinx.serialization.DeserializationStrategy @@ -545,6 +544,13 @@ sealed class Route( object FollowupMessageDelete : Route(HttpMethod.Delete, "/webhooks/${ApplicationId}/${InteractionToken}/messages/${MessageId}", 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/GuildService.kt b/rest/src/main/kotlin/service/GuildService.kt index de52937f7d4..3b5645fc12d 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 @@ -314,6 +311,18 @@ 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) suspend inline fun GuildService.modifyGuildWelcomeScreen(guildId: Snowflake, builder: WelcomeScreenModifyBuilder.() -> Unit): DiscordWelcomeScreen { @@ -340,3 +349,18 @@ suspend inline fun GuildService.createCategory(guildId: Snowflake, name: String, 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, builder: VoiceStateModifyBuilder.() -> Unit) { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + val modifyBuilder = VoiceStateModifyBuilder(guildId).apply(builder) + modifyVoiceState(guildId,channelId, modifyBuilder.toRequest()) +}