From 2c0bce664e66bdfb69c23a4354586ca8619ee0be Mon Sep 17 00:00:00 2001 From: HopeBaron Date: Fri, 9 Jul 2021 05:26:30 +0300 Subject: [PATCH 01/55] thread gateway/rest implementation --- .../src/main/kotlin/entity/DiscordChannel.kt | 28 +++++++- common/src/main/kotlin/entity/DiscordGuild.kt | 1 + .../src/main/kotlin/entity/DiscordMessage.kt | 9 ++- common/src/main/kotlin/entity/Member.kt | 10 +++ common/src/main/kotlin/entity/Permission.kt | 8 ++- gateway/src/main/kotlin/Event.kt | 68 +++++++++++++++++++ rest/src/main/kotlin/json/JsonErrorCode.kt | 8 ++- rest/src/main/kotlin/json/response/Channel.kt | 14 +++- rest/src/main/kotlin/route/Route.kt | 68 +++++++++++++++++++ 9 files changed, 208 insertions(+), 6 deletions(-) diff --git a/common/src/main/kotlin/entity/DiscordChannel.kt b/common/src/main/kotlin/entity/DiscordChannel.kt index 5fb9f3f522d5..43b16638be0a 100644 --- a/common/src/main/kotlin/entity/DiscordChannel.kt +++ b/common/src/main/kotlin/entity/DiscordChannel.kt @@ -65,7 +65,13 @@ data class DiscordChannel( val parentId: OptionalSnowflake? = OptionalSnowflake.Missing, @SerialName("last_pin_timestamp") val lastPinTimestamp: Optional = Optional.Missing(), - val permissions: Optional = Optional.Missing() + val permissions: Optional = Optional.Missing(), + @SerialName("message_count") + val messageCount: OptionalInt = OptionalInt.Missing, + @SerialName("member_count") + val memberCount: OptionalInt = OptionalInt.Missing, + val threadMetadata: Optional = Optional.Missing(), + val member: Optional = Optional.Missing() ) @Serializable(with = ChannelType.Serializer::class) @@ -94,6 +100,12 @@ sealed class ChannelType(val value: Int) { /** A channel in which game developers can sell their game on Discord. */ object GuildStore : ChannelType(6) + object NewsThread : ChannelType(10) + + object PrivateThread : ChannelType(11) + + object PublicThread : ChannelType(12) + object GuildStageVoice : ChannelType(13) companion object; @@ -110,6 +122,9 @@ sealed class ChannelType(val value: Int) { 4 -> GuildCategory 5 -> GuildNews 6 -> GuildStore + 10 -> NewsThread + 11 -> PrivateThread + 12 -> PublicThread 13 -> GuildStageVoice else -> Unknown(code) } @@ -154,3 +169,14 @@ sealed class OverwriteType(val value: Int) { } } } + +@Serializable +class DiscordThreadMetadata( + val archived: Boolean, + @SerialName("archiver_id") + val archiverId: OptionalSnowflake = OptionalSnowflake.Missing, + @SerialName("archive_timestamp") + val archiveTimestamp: String, + val autoArchiveDuration: Int, + val locked: OptionalBoolean = OptionalBoolean.Missing +) \ No newline at end of file diff --git a/common/src/main/kotlin/entity/DiscordGuild.kt b/common/src/main/kotlin/entity/DiscordGuild.kt index 2b77282e92d9..ee83f8b99f3e 100644 --- a/common/src/main/kotlin/entity/DiscordGuild.kt +++ b/common/src/main/kotlin/entity/DiscordGuild.kt @@ -128,6 +128,7 @@ data class DiscordGuild( val voiceStates: Optional> = Optional.Missing(), val members: Optional> = Optional.Missing(), val channels: Optional> = Optional.Missing(), + val threads: Optional> = Optional.Missing(), val presences: Optional> = Optional.Missing(), @SerialName("max_presences") val maxPresences: OptionalInt? = OptionalInt.Missing, diff --git a/common/src/main/kotlin/entity/DiscordMessage.kt b/common/src/main/kotlin/entity/DiscordMessage.kt index dfaca1529502..3e24033b0a75 100644 --- a/common/src/main/kotlin/entity/DiscordMessage.kt +++ b/common/src/main/kotlin/entity/DiscordMessage.kt @@ -109,7 +109,8 @@ data class DiscordMessage( * This is a list even though the docs say it's a component */ val components: Optional> = Optional.Missing(), - val interaction: Optional = Optional.Missing() + val interaction: Optional = Optional.Missing(), + val thread: Optional = Optional.Missing() ) /** @@ -298,7 +299,11 @@ enum class MessageFlag(val code: Int) { /* This message came from the urgent message system. */ Urgent(16), - Ephemeral(64); + HasThread(32), + + Ephemeral(64), + + Loading(128); } @Serializable(with = MessageFlags.Serializer::class) diff --git a/common/src/main/kotlin/entity/Member.kt b/common/src/main/kotlin/entity/Member.kt index 32d0e4aa500b..8ed51520e3ad 100644 --- a/common/src/main/kotlin/entity/Member.kt +++ b/common/src/main/kotlin/entity/Member.kt @@ -82,3 +82,13 @@ data class DiscordUpdatedGuildMember( val premiumSince: Optional = Optional.Missing(), val pending: OptionalBoolean = OptionalBoolean.Missing ) + +@Serializable +data class DiscordThreadMember( + val id: Snowflake, + @SerialName("user_id") + val userId: Snowflake, + @SerialName("join_timestamp") + val joinTimestamp: String, + val flags: Int +) \ No newline at end of file diff --git a/common/src/main/kotlin/entity/Permission.kt b/common/src/main/kotlin/entity/Permission.kt index f9cb20f34263..11ce108cfcbb 100644 --- a/common/src/main/kotlin/entity/Permission.kt +++ b/common/src/main/kotlin/entity/Permission.kt @@ -156,6 +156,9 @@ sealed class Permission(val code: DiscordBitSet) { object ManageEmojis : Permission(0x40000000) object UseSlashCommands : Permission(0x80000000) object RequestToSpeak : Permission(0x100000000) + object ManageThreads : Permission(0x0400000000) + object UsePublicThreads : Permission(0x0800000000) + object UsePrivateThreads : Permission(0x1000000000) object All : Permission(values.fold(EmptyBitSet()) { acc, value -> acc.add(value.code); acc }) companion object { @@ -192,7 +195,10 @@ sealed class Permission(val code: DiscordBitSet) { ManageWebhooks, ManageEmojis, UseSlashCommands, - RequestToSpeak + RequestToSpeak, + ManageThreads, + UsePublicThreads, + UsePrivateThreads, ) } } diff --git a/gateway/src/main/kotlin/Event.kt b/gateway/src/main/kotlin/Event.kt index 5e94a94f9517..00437c0f3f5c 100644 --- a/gateway/src/main/kotlin/Event.kt +++ b/gateway/src/main/kotlin/Event.kt @@ -381,6 +381,40 @@ sealed class Event { sequence ) + "THREAD_CREATE" -> ThreadCreate( + decoder.decodeSerializableElement(descriptor, index, DiscordChannel.serializer()), + sequence + ) + + + "THREAD_DELETE" -> ThreadDelete( + decoder.decodeSerializableElement(descriptor, index, DiscordChannel.serializer()), + sequence + ) + + + "THREAD_UPDATE" -> ThreadUpdate( + decoder.decodeSerializableElement(descriptor, index, DiscordChannel.serializer()), + sequence + ) + + "THREAD_LIST_SYNC" -> ThreadListSync( + decoder.decodeSerializableElement(descriptor, index, DiscordThreadListSync.serializer()), + sequence + ) + + "THREAD_MEMBER_UPDATE" -> ThreadMemberUpdate( + decoder.decodeSerializableElement(descriptor, index, DiscordThreadMember.serializer()), + sequence + ) + + + "THREAD_MEMBERS_UPDATE" -> ThreadMembersUpdate( + decoder.decodeSerializableElement(descriptor, index, DiscordThreadMembersUpdate.serializer()), + sequence + ) + + else -> { jsonLogger.warn { "unknown gateway event name $name" } @@ -633,3 +667,37 @@ data class ApplicationCommandUpdate(val application: DiscordApplicationCommand, @KordPreview data class ApplicationCommandDelete(val application: DiscordApplicationCommand, override val sequence: Int?) : DispatchEvent() + +data class ThreadCreate(val channel: DiscordChannel, override val sequence: Int?) : DispatchEvent() + +data class ThreadUpdate(val channel: DiscordChannel, override val sequence: Int?) : DispatchEvent() + +data class ThreadDelete(val channel: DiscordChannel, override val sequence: Int?) : DispatchEvent() + +data class ThreadMemberUpdate(val member: DiscordThreadMember, override val sequence: Int?) : DispatchEvent() + +data class ThreadListSync(val sync: DiscordThreadListSync, override val sequence: Int?) : DispatchEvent() + +data class ThreadMembersUpdate(val members: DiscordThreadMembersUpdate, override val sequence: Int?) : DispatchEvent() + +@Serializable +data class DiscordThreadListSync( + @SerialName("guild_id") + val guildId: Snowflake, + @SerialName("channel_ids") + val channelIds: List, + val theads: List, + val members: List +) + +@Serializable +data class DiscordThreadMembersUpdate( + val id: Snowflake, + @SerialName("guild_id") + val guildId: Snowflake, + @SerialName("member_count") + val memberCount: Int, + @SerialName("added_members") + val addedMembers: Optional> = Optional.Missing(), + val removedMemberIds: Optional> = Optional.Missing() +) \ No newline at end of file diff --git a/rest/src/main/kotlin/json/JsonErrorCode.kt b/rest/src/main/kotlin/json/JsonErrorCode.kt index 42c76bef02ff..7755f9ce7414 100644 --- a/rest/src/main/kotlin/json/JsonErrorCode.kt +++ b/rest/src/main/kotlin/json/JsonErrorCode.kt @@ -521,7 +521,13 @@ enum class JsonErrorCode(val code: Int) { * * [JSON Error Codes](https://github.com/discord/discord-api-docs/blob/master/docs/topics/Opcodes_and_Status_Codes.md#json-error-codes) */ - APIResourceOverloaded(130000); + APIResourceOverloaded(130000), + + OperationOnAchievedThread(50083), + + InvalidThreadSettings(50084), + + InvalidThreadBefore(50085),; companion object JsonErrorCodeSerializer : KSerializer { override val descriptor = PrimitiveSerialDescriptor("JsonErrorCodeSerializer", PrimitiveKind.INT) diff --git a/rest/src/main/kotlin/json/response/Channel.kt b/rest/src/main/kotlin/json/response/Channel.kt index 4568e5520c52..04139f878d21 100644 --- a/rest/src/main/kotlin/json/response/Channel.kt +++ b/rest/src/main/kotlin/json/response/Channel.kt @@ -1,7 +1,19 @@ package dev.kord.rest.json.response import dev.kord.common.entity.ChannelType +import dev.kord.common.entity.DiscordChannel +import dev.kord.common.entity.DiscordThreadMember +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class PartialChannelResponse(val name: String, val type: ChannelType) \ No newline at end of file +data class PartialChannelResponse(val name: String, val type: ChannelType) + + +@Serializable +data class ListThreadsResponse( + val threads: List, + val members: List, + @SerialName("has_more") + val hasMore: Boolean +) \ 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 0cd5858b3602..b99d34a37edf 100644 --- a/rest/src/main/kotlin/route/Route.kt +++ b/rest/src/main/kotlin/route/Route.kt @@ -684,6 +684,74 @@ sealed class Route( object StageInstanceDelete : Route(HttpMethod.Delete, "/stage-instances/$ChannelId", NoStrategy) + object StartPublicThreadPost : + Route( + HttpMethod.Post, + "/channels/${ChannelId}/messages/${MessageId}/threads", + DiscordChannel.serializer() + ); + + + object StartPrivateThreadPost : + Route(HttpMethod.Post, "/channels/${ChannelId}/threads", DiscordChannel.serializer()); + + object JoinThreadPut : + Route(HttpMethod.Put, "/channels/${ChannelId}/thread-members/@me", NoStrategy) + + object AddThreadMemberPut : + Route(HttpMethod.Put, "/channels/$ChannelId/thread-members/${UserId}", NoStrategy) + + object LeaveThreadDelete : + Route(HttpMethod.Delete, "/channels/${ChannelId}/thread-members/@me", NoStrategy) + + object RemoveUserFromThreadDelete : + Route(HttpMethod.Delete, "/channels/${ChannelId}/thread-members/${UserId}", NoStrategy) + + object ListThreadMembersGet : + Route>( + HttpMethod.Get, + "/channels/${ChannelId}/thread-members", + ListSerializer(DiscordThreadMember.serializer()) + ) + + object ListActiveThreadsGet : + Route( + HttpMethod.Get, + "/channels/${ChannelId}/threads/active", + ListThreadsResponse.serializer() + ) + + + object ListPrivateThreadsGet : + Route( + HttpMethod.Get, + "/channels/${ChannelId}/threads/private", + ListThreadsResponse.serializer() + ) + + + object ListPrivateArchivedThreadsGet : + Route( + HttpMethod.Get, + "/channels/${ChannelId}/threads/archived/private", + ListThreadsResponse.serializer() + ) + + object ListPublicArchivedThreadsGet : + Route( + HttpMethod.Get, + "/channels/${ChannelId}/threads/archived/public", + ListThreadsResponse.serializer() + ) + + object ListJoinedPrivateArchivedThreadsGet : + Route( + HttpMethod.Get, + "/channels/$ChannelId/users/@me/threads/archived/private", + ListThreadsResponse.serializer() + ) + + companion object { val baseUrl = "https://discord.com/api/$restVersion" } From 544f742b027b42dd14a4098bd7dee1d47a6e6c9a Mon Sep 17 00:00:00 2001 From: HopeBaron Date: Fri, 9 Jul 2021 05:34:25 +0300 Subject: [PATCH 02/55] add service endpoints and channel requests --- .../kotlin/json/request/ChannelRequests.kt | 39 ++++++++ .../src/main/kotlin/service/ChannelService.kt | 95 +++++++++++++++++++ 2 files changed, 134 insertions(+) diff --git a/rest/src/main/kotlin/json/request/ChannelRequests.kt b/rest/src/main/kotlin/json/request/ChannelRequests.kt index bc3abe81114b..b791c839347c 100644 --- a/rest/src/main/kotlin/json/request/ChannelRequests.kt +++ b/rest/src/main/kotlin/json/request/ChannelRequests.kt @@ -8,8 +8,14 @@ import dev.kord.common.entity.optional.Optional import dev.kord.common.entity.optional.OptionalBoolean import dev.kord.common.entity.optional.OptionalInt import dev.kord.common.entity.optional.OptionalSnowflake +import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder @Serializable data class ChannelModifyPutRequest( @@ -51,3 +57,36 @@ data class ChannelPermissionEditRequest( val deny: Permissions, val type: OverwriteType ) + +@Serializable +data class StartThreadRequest( + val name: String, + val autoArchiveDuration: Int +) + +enum class ArchieveDuration(val duration: Int) { + Unknown(Int.MAX_VALUE), + Hour(60), + Day(1440), + ThreeDays(4320), + Week(10080); + + object Serializer : KSerializer { + override fun deserialize(decoder: Decoder): ArchieveDuration { + val value = decoder.decodeInt() + return values().firstOrNull { it.duration == value } ?: Unknown + } + + override val descriptor: SerialDescriptor + get() = PrimitiveSerialDescriptor("AutoArchieveDuration", PrimitiveKind.INT) + + override fun serialize(encoder: Encoder, value: ArchieveDuration) { + encoder.encodeInt(value.duration) + } + } +} + +data class ListThreadsRequest( + val before: Snowflake? = null, + val limit: Int? = null +) diff --git a/rest/src/main/kotlin/service/ChannelService.kt b/rest/src/main/kotlin/service/ChannelService.kt index 5ef9286f2b13..be2339cfbee0 100644 --- a/rest/src/main/kotlin/service/ChannelService.kt +++ b/rest/src/main/kotlin/service/ChannelService.kt @@ -7,6 +7,7 @@ import dev.kord.rest.builder.message.MessageCreateBuilder import dev.kord.rest.builder.message.MessageModifyBuilder import dev.kord.rest.json.request.* import dev.kord.rest.json.response.FollowedChannelResponse +import dev.kord.rest.json.response.ListThreadsResponse import dev.kord.rest.request.RequestHandler import dev.kord.rest.route.Position import dev.kord.rest.route.Route @@ -233,6 +234,100 @@ class ChannelService(requestHandler: RequestHandler) : RestService(requestHandle keys[Route.ChannelId] = channelId body(ChannelFollowRequest.serializer(), request) } + suspend fun startPublicThread( + channelId: Snowflake, + messageId: Snowflake, + request: StartThreadRequest + ): DiscordChannel { + return call(Route.StartPublicThreadPost) { + keys[Route.ChannelId] = channelId + keys[Route.MessageId] = messageId + body(StartThreadRequest.serializer(), request) + } + } + + + suspend fun startPrivateThread( + channelId: Snowflake, + request: StartThreadRequest + ): DiscordChannel { + return call(Route.StartPrivateThreadPost) { + keys[Route.ChannelId] = channelId + body(StartThreadRequest.serializer(), request) + } + } + + suspend fun joinThread(channelId: Snowflake) { + call(Route.JoinThreadPut) { + keys[Route.ChannelId] = channelId + } + } + + suspend fun addUserToThread(channelId: Snowflake, userId: Snowflake) { + call(Route.AddThreadMemberPut) { + keys[Route.ChannelId] = channelId + keys[Route.UserId] = userId + } + } + suspend fun leaveThread(channelId: Snowflake) { + call(Route.LeaveThreadDelete) { + keys[Route.ChannelId] = channelId + } + } + + suspend fun removeUserFromThread(channelId: Snowflake, userId: Snowflake) { + call(Route.RemoveUserFromThreadDelete) { + keys[Route.ChannelId] = channelId + keys[Route.UserId] = userId + } + } + + suspend fun listThreadMembers(channelId: Snowflake): List { + return call(Route.ListThreadMembersGet) { + keys[Route.ChannelId] = channelId + } + } + + suspend fun listActiveThreads(channelId: Snowflake): ListThreadsResponse { + return call(Route.ListActiveThreadsGet) { + keys[Route.ChannelId] = channelId + + } + } + + suspend fun listPublicArchivedThreads(channelId: Snowflake, request: ListThreadsRequest): ListThreadsResponse { + return call(Route.ListPublicArchivedThreadsGet) { + keys[Route.ChannelId] = channelId + val before = request.before + val limit = request.limit + if(before != null) parameter("before", before) + if(limit != null) parameter("limit", limit) + + } + } + + suspend fun listPrivateArchivedThreads(channelId: Snowflake, request: ListThreadsRequest): ListThreadsResponse { + return call(Route.ListActiveThreadsGet) { + keys[Route.ChannelId] = channelId + val before = request.before + val limit = request.limit + if(before != null) parameter("before", before) + if(limit != null) parameter("limit", limit) + + } + } + + suspend fun listJoinedPrivateArchivedThreads(channelId: Snowflake, request: ListThreadsRequest): ListThreadsResponse { + return call(Route.ListActiveThreadsGet) { + keys[Route.ChannelId] = channelId + val before = request.before + val limit = request.limit + if(before != null) parameter("before", before) + if(limit != null) parameter("limit", limit) + + } + + } } From 58a91a3ba4b938f753c96a9498d5e3cbe489c437 Mon Sep 17 00:00:00 2001 From: HopeBaron Date: Fri, 9 Jul 2021 05:51:53 +0300 Subject: [PATCH 03/55] split invite from categorizables --- .../entity/channel/CategorizableChannel.kt | 14 ---------- .../kotlin/entity/channel/InviteChannel.kt | 26 +++++++++++++++++++ 2 files changed, 26 insertions(+), 14 deletions(-) create mode 100644 core/src/main/kotlin/entity/channel/InviteChannel.kt diff --git a/core/src/main/kotlin/entity/channel/CategorizableChannel.kt b/core/src/main/kotlin/entity/channel/CategorizableChannel.kt index 08292f8671d6..d2a83d53b8af 100644 --- a/core/src/main/kotlin/entity/channel/CategorizableChannel.kt +++ b/core/src/main/kotlin/entity/channel/CategorizableChannel.kt @@ -35,17 +35,3 @@ interface CategorizableChannel : GuildChannel { } -/** - * Request to create an invite for this channel. - * - * @return the created [Invite]. - * @throws RestRequestException if something went wrong during the request. - */ -@OptIn(ExperimentalContracts::class) -suspend inline fun CategorizableChannel.createInvite(builder: InviteCreateBuilder.() -> Unit = {}): Invite { - contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } - val response = kord.rest.channel.createInvite(id, builder) - val data = InviteData.from(response) - - return Invite(data, kord) -} \ No newline at end of file diff --git a/core/src/main/kotlin/entity/channel/InviteChannel.kt b/core/src/main/kotlin/entity/channel/InviteChannel.kt new file mode 100644 index 000000000000..84ccb326c69d --- /dev/null +++ b/core/src/main/kotlin/entity/channel/InviteChannel.kt @@ -0,0 +1,26 @@ +package dev.kord.core.entity.channel + +import dev.kord.core.cache.data.InviteData +import dev.kord.core.entity.Invite +import dev.kord.rest.builder.channel.InviteCreateBuilder +import dev.kord.rest.request.RestRequestException +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +interface InviteChannel : GuildChannel + +/** + * Request to create an invite for this channel. + * + * @return the created [Invite]. + * @throws RestRequestException if something went wrong during the request. + */ +@OptIn(ExperimentalContracts::class) +suspend inline fun InviteChannel.createInvite(builder: InviteCreateBuilder.() -> Unit = {}): Invite { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + val response = kord.rest.channel.createInvite(id, builder) + val data = InviteData.from(response) + + return Invite(data, kord) +} \ No newline at end of file From 816e7a2dce7cef0f8584d3d8949f75c8746d59e4 Mon Sep 17 00:00:00 2001 From: HopeBaron Date: Fri, 9 Jul 2021 19:00:41 +0300 Subject: [PATCH 04/55] Add threads, thread users, missing routes There's still more modifications to do but hopefully we are close to a milestone in this regards. --- .../behavior/channel/TextChannelBehavior.kt | 3 ++ .../channel/threads/ThreadBehavior.kt | 47 +++++++++++++++++++ .../threads/ThreadParentChannelBehavior.kt | 14 ++++++ .../src/main/kotlin/cache/data/ChannelData.kt | 26 +++++++++- .../main/kotlin/cache/data/ThreadUserData.kt | 22 +++++++++ .../entity/channel/CategorizableChannel.kt | 14 ++++++ .../kotlin/entity/channel/thread/Thread.kt | 32 +++++++++++++ .../entity/channel/thread/ThreadUser.kt | 15 ++++++ .../main/kotlin/supplier/EntitySupplier.kt | 12 +++++ .../channel/thread/ThreadModifyBuilder.kt | 38 +++++++++++++++ .../kotlin/json/request/ThreadRequests.kt | 17 +++++++ rest/src/main/kotlin/route/Route.kt | 2 +- .../src/main/kotlin/service/ChannelService.kt | 7 +++ 13 files changed, 246 insertions(+), 3 deletions(-) create mode 100644 core/src/main/kotlin/behavior/channel/threads/ThreadBehavior.kt create mode 100644 core/src/main/kotlin/behavior/channel/threads/ThreadParentChannelBehavior.kt create mode 100644 core/src/main/kotlin/cache/data/ThreadUserData.kt create mode 100644 core/src/main/kotlin/entity/channel/thread/Thread.kt create mode 100644 core/src/main/kotlin/entity/channel/thread/ThreadUser.kt create mode 100644 rest/src/main/kotlin/builder/channel/thread/ThreadModifyBuilder.kt create mode 100644 rest/src/main/kotlin/json/request/ThreadRequests.kt diff --git a/core/src/main/kotlin/behavior/channel/TextChannelBehavior.kt b/core/src/main/kotlin/behavior/channel/TextChannelBehavior.kt index 52ab3c73cc70..ee411a21b2d7 100644 --- a/core/src/main/kotlin/behavior/channel/TextChannelBehavior.kt +++ b/core/src/main/kotlin/behavior/channel/TextChannelBehavior.kt @@ -12,6 +12,9 @@ import dev.kord.core.supplier.EntitySupplyStrategy import dev.kord.rest.builder.channel.TextChannelModifyBuilder import dev.kord.rest.request.RestRequestException import dev.kord.rest.service.patchTextChannel +import dev.kord.core.entity.channel.thread.Thread +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow import java.util.* import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind diff --git a/core/src/main/kotlin/behavior/channel/threads/ThreadBehavior.kt b/core/src/main/kotlin/behavior/channel/threads/ThreadBehavior.kt new file mode 100644 index 000000000000..20bf83d7cf3c --- /dev/null +++ b/core/src/main/kotlin/behavior/channel/threads/ThreadBehavior.kt @@ -0,0 +1,47 @@ +package dev.kord.core.behavior.channel.threads + +import dev.kord.common.entity.Snowflake +import dev.kord.core.behavior.channel.GuildChannelBehavior +import dev.kord.core.cache.data.ThreadUserData +import dev.kord.core.cache.data.toData +import dev.kord.core.entity.channel.thread.Thread +import dev.kord.core.entity.channel.thread.ThreadUser +import dev.kord.rest.builder.channel.thread.ThreadModifyBuilder +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +interface ThreadBehavior : GuildChannelBehavior { + + val members: Flow + get() = flow { + val threadUsers = supplier.getThreadMembersOrNull(id) + + suspend fun removeUser(userId: Snowflake) { + kord.rest.channel.removeUserFromThread(id, userId) + } + + suspend fun addUser(userId: Snowflake) { + kord.rest.channel.addUserToThread(id, userId) + } + + suspend fun join() { + kord.rest.channel.joinThread(id) + } + + suspend fun leave() { + kord.rest.channel.leaveThread(id) + } + +} + +@OptIn(ExperimentalContracts::class) +suspend inline fun ThreadBehavior.edit(builder: ThreadModifyBuilder.() -> Unit): Thread { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + val appliedBuilder = ThreadModifyBuilder().apply(builder) + val patchedChannel = kord.rest.channel.patchThread(id, appliedBuilder.toRequest(), appliedBuilder.reason) + val patchedChannelData = patchedChannel.toData() + return Thread(patchedChannelData, kord) +} \ No newline at end of file diff --git a/core/src/main/kotlin/behavior/channel/threads/ThreadParentChannelBehavior.kt b/core/src/main/kotlin/behavior/channel/threads/ThreadParentChannelBehavior.kt new file mode 100644 index 000000000000..510c3d7659dd --- /dev/null +++ b/core/src/main/kotlin/behavior/channel/threads/ThreadParentChannelBehavior.kt @@ -0,0 +1,14 @@ +package dev.kord.core.behavior.channel.threads + +import dev.kord.core.behavior.channel.GuildChannelBehavior +import dev.kord.core.entity.channel.thread.Thread +import kotlinx.coroutines.flow.Flow + +interface ThreadParentChannelBehavior : GuildChannelBehavior { + + val publicActiveThreads: Flow get() = supplier.getActiveThreads(id) + + val publicArchievedThreads: Flow get() = supplier.getPublicArchivedThreads(id) + + +} \ No newline at end of file diff --git a/core/src/main/kotlin/cache/data/ChannelData.kt b/core/src/main/kotlin/cache/data/ChannelData.kt index 2923d0ae5b98..625fc1c0e178 100644 --- a/core/src/main/kotlin/cache/data/ChannelData.kt +++ b/core/src/main/kotlin/cache/data/ChannelData.kt @@ -3,6 +3,7 @@ package dev.kord.core.cache.data import dev.kord.cache.api.data.description import dev.kord.common.entity.* import dev.kord.common.entity.optional.* +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable @@ -25,7 +26,9 @@ data class ChannelData( val applicationId: OptionalSnowflake = OptionalSnowflake.Missing, val parentId: OptionalSnowflake? = OptionalSnowflake.Missing, val lastPinTimestamp: Optional = Optional.Missing(), - val permissions: Optional = Optional.Missing() + val permissions: Optional = Optional.Missing(), + @SerialName("thread_metadata") + val threadsMetadata: Optional = Optional.Missing() ) { @@ -52,11 +55,30 @@ data class ChannelData( applicationId, parentId, lastPinTimestamp, - permissions + permissions, + threadMetadata.map { ThreadMetadata.from(it) } ) } } } +@Serializable +data class ThreadMetadata( + val archived: Boolean, + @SerialName("archiver_id") + val archiverId: OptionalSnowflake = OptionalSnowflake.Missing, + @SerialName("archive_timestamp") + val archiveTimestamp: String, + val autoArchiveDuration: Int, + val locked: OptionalBoolean = OptionalBoolean.Missing +) { + companion object { + fun from(threadMetadata: DiscordThreadMetadata): ThreadMetadata = with(threadMetadata) { + ThreadMetadata(archived, archiverId, archiveTimestamp, autoArchiveDuration, locked) + } + } +} + + fun DiscordChannel.toData() = ChannelData.from(this) \ No newline at end of file diff --git a/core/src/main/kotlin/cache/data/ThreadUserData.kt b/core/src/main/kotlin/cache/data/ThreadUserData.kt new file mode 100644 index 000000000000..601e7e4de4af --- /dev/null +++ b/core/src/main/kotlin/cache/data/ThreadUserData.kt @@ -0,0 +1,22 @@ +package dev.kord.core.cache.data + +import dev.kord.common.entity.DiscordThreadMember +import dev.kord.common.entity.Snowflake +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ThreadUserData( + val id: Snowflake, + @SerialName("user_id") + val userId: Snowflake, + @SerialName("join_timestamp") + val joinTimestamp: String, + val flags: Int +) { + companion object { + fun from(data: DiscordThreadMember): ThreadUserData = with(data) { + ThreadUserData(id, userId, joinTimestamp, flags) + } + } +} diff --git a/core/src/main/kotlin/entity/channel/CategorizableChannel.kt b/core/src/main/kotlin/entity/channel/CategorizableChannel.kt index d2a83d53b8af..08292f8671d6 100644 --- a/core/src/main/kotlin/entity/channel/CategorizableChannel.kt +++ b/core/src/main/kotlin/entity/channel/CategorizableChannel.kt @@ -35,3 +35,17 @@ interface CategorizableChannel : GuildChannel { } +/** + * Request to create an invite for this channel. + * + * @return the created [Invite]. + * @throws RestRequestException if something went wrong during the request. + */ +@OptIn(ExperimentalContracts::class) +suspend inline fun CategorizableChannel.createInvite(builder: InviteCreateBuilder.() -> Unit = {}): Invite { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + val response = kord.rest.channel.createInvite(id, builder) + val data = InviteData.from(response) + + return Invite(data, kord) +} \ No newline at end of file diff --git a/core/src/main/kotlin/entity/channel/thread/Thread.kt b/core/src/main/kotlin/entity/channel/thread/Thread.kt new file mode 100644 index 000000000000..c74046cee53e --- /dev/null +++ b/core/src/main/kotlin/entity/channel/thread/Thread.kt @@ -0,0 +1,32 @@ +package dev.kord.core.entity.channel.thread + +import dev.kord.common.entity.Snowflake +import dev.kord.common.entity.optional.value +import dev.kord.core.Kord +import dev.kord.core.behavior.channel.threads.ThreadBehavior +import dev.kord.core.cache.data.ChannelData +import dev.kord.core.supplier.EntitySupplier + +class Thread(val data: ChannelData, override val kord: Kord, override val supplier: EntitySupplier) : ThreadBehavior { + + private val threadData = data.threadsMetadata.value!! + + override val id: Snowflake get() = data.id + + override val guildId: Snowflake get() = data.guildId.value!! + + val name: String get() = data.name.value!! + + val archived: Boolean get() = threadData.archived + + val locked: Boolean get() = threadData.locked.orElse(false) + + val archiverId: Snowflake? get() = threadData.archiverId.value + + val archiveTimeStamps: String get() = threadData.archiveTimestamp + + val autoArchiveDuration: Int get() = threadData.autoArchiveDuration + + val ratelimitPerUser: Int? get() = data.rateLimitPerUser.value + +} \ No newline at end of file diff --git a/core/src/main/kotlin/entity/channel/thread/ThreadUser.kt b/core/src/main/kotlin/entity/channel/thread/ThreadUser.kt new file mode 100644 index 000000000000..3c6d913574d0 --- /dev/null +++ b/core/src/main/kotlin/entity/channel/thread/ThreadUser.kt @@ -0,0 +1,15 @@ +package dev.kord.core.entity.channel.thread + +import dev.kord.common.entity.Snowflake +import dev.kord.core.Kord +import dev.kord.core.behavior.UserBehavior +import dev.kord.core.cache.data.ThreadUserData +import dev.kord.core.supplier.EntitySupplier + +data class ThreadUser( + val data: ThreadUserData, + override val kord: Kord, + override val supplier: EntitySupplier = kord.defaultSupplier +) : UserBehavior { + override val id: Snowflake get() = data.userId +} diff --git a/core/src/main/kotlin/supplier/EntitySupplier.kt b/core/src/main/kotlin/supplier/EntitySupplier.kt index f059f4da729c..63470dcbad12 100644 --- a/core/src/main/kotlin/supplier/EntitySupplier.kt +++ b/core/src/main/kotlin/supplier/EntitySupplier.kt @@ -7,6 +7,8 @@ import dev.kord.core.entity.* import dev.kord.core.entity.channel.Channel import dev.kord.core.entity.channel.GuildChannel import dev.kord.core.entity.channel.MessageChannel +import dev.kord.core.entity.channel.thread.Thread +import dev.kord.core.entity.channel.thread.ThreadUser import dev.kord.core.exception.EntityNotFoundException import kotlinx.coroutines.flow.Flow @@ -394,6 +396,16 @@ interface EntitySupplier { suspend fun getStageInstance(channelId: Snowflake): StageInstance = getStageInstanceOrNull(channelId) ?: EntityNotFoundException.stageInstanceNotFound(channelId) + + fun getThreadMembersOrNull(channelId: Snowflake): Flow + + fun getActiveThreads(channelId: Snowflake): Flow + + fun getPublicArchivedThreads(channelId: Snowflake): Flow + + fun getPrivateArchivedThreads(channelId: Snowflake): Flow + + fun getJoinedPrivateArchivedThreads(channelId: Snowflake): Flow } diff --git a/rest/src/main/kotlin/builder/channel/thread/ThreadModifyBuilder.kt b/rest/src/main/kotlin/builder/channel/thread/ThreadModifyBuilder.kt new file mode 100644 index 000000000000..f0c699fa4a62 --- /dev/null +++ b/rest/src/main/kotlin/builder/channel/thread/ThreadModifyBuilder.kt @@ -0,0 +1,38 @@ +package dev.kord.rest.builder.channel.thread + +import dev.kord.common.entity.optional.Optional +import dev.kord.common.entity.optional.OptionalBoolean +import dev.kord.common.entity.optional.OptionalInt +import dev.kord.common.entity.optional.delegate.delegate +import dev.kord.rest.builder.AuditRequestBuilder +import dev.kord.rest.json.request.ThreadModifyPatchRequest + +class ThreadModifyBuilder : AuditRequestBuilder { + + private var _name: Optional = Optional.Missing() + var name: String? by ::_name.delegate() + + private var _archived: OptionalBoolean = OptionalBoolean.Missing + var archived: Boolean? by ::_archived.delegate() + + private var _locked: OptionalBoolean = OptionalBoolean.Missing + var locked: Boolean? = null + + private var _rateLimitPerUser: OptionalInt = OptionalInt.Missing + var rateLimitPerUser: Int? by ::_rateLimitPerUser.delegate() + + private var _autoArchiveDuration: OptionalInt = OptionalInt.Missing + var autoArchiveDuration: Int? by ::_autoArchiveDuration.delegate() + + override fun toRequest(): ThreadModifyPatchRequest { + return ThreadModifyPatchRequest( + name = _name, + locked = _locked, + archived = _archived, + autoArchiveDuration = _autoArchiveDuration, + ratelimitPerUser = _rateLimitPerUser + ) + } + + override var reason: String? = null +} \ No newline at end of file diff --git a/rest/src/main/kotlin/json/request/ThreadRequests.kt b/rest/src/main/kotlin/json/request/ThreadRequests.kt new file mode 100644 index 000000000000..9cf69e4f8ee2 --- /dev/null +++ b/rest/src/main/kotlin/json/request/ThreadRequests.kt @@ -0,0 +1,17 @@ +package dev.kord.rest.json.request + +import dev.kord.common.entity.optional.Optional +import dev.kord.common.entity.optional.OptionalBoolean +import dev.kord.common.entity.optional.OptionalInt +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ThreadModifyPatchRequest( + val name: Optional = Optional.Missing(), + val archived: OptionalBoolean = OptionalBoolean.Missing, + val autoArchiveDuration: OptionalInt = OptionalInt.Missing, + val locked: OptionalBoolean = OptionalBoolean.Missing, + @SerialName("rate_limit_per_user") + val ratelimitPerUser: OptionalInt = OptionalInt.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 b99d34a37edf..c79c614a400a 100644 --- a/rest/src/main/kotlin/route/Route.kt +++ b/rest/src/main/kotlin/route/Route.kt @@ -19,7 +19,7 @@ import kotlinx.serialization.serializer import dev.kord.common.entity.DiscordEmoji as EmojiEntity internal const val REST_VERSION_PROPERTY_NAME = "dev.kord.rest.version" -internal val restVersion get() = System.getenv(REST_VERSION_PROPERTY_NAME) ?: "v8" +internal val restVersion get() = System.getenv(REST_VERSION_PROPERTY_NAME) ?: "v9" sealed interface ResponseMapper { fun deserialize(json: Json, body: String): T diff --git a/rest/src/main/kotlin/service/ChannelService.kt b/rest/src/main/kotlin/service/ChannelService.kt index be2339cfbee0..7459a2cb16ba 100644 --- a/rest/src/main/kotlin/service/ChannelService.kt +++ b/rest/src/main/kotlin/service/ChannelService.kt @@ -221,6 +221,13 @@ class ChannelService(requestHandler: RequestHandler) : RestService(requestHandle body(ChannelModifyPatchRequest.serializer(), channel) reason?.let { header("X-Audit-Log-Reason", reason) } } + suspend fun patchThread(threadId: Snowflake, thread: ThreadModifyPatchRequest, reason: String? = null) = + call(Route.ChannelPatch) { + keys[Route.ChannelId] = threadId + body(ThreadModifyPatchRequest.serializer(), thread) + reason?.let { header("X-Audit-Log-Reason", reason) } + } + @KordPreview suspend fun crossPost(channelId: Snowflake, messageId: Snowflake): DiscordMessage = call(Route.MessageCrosspost) { From 844cb35deaaea735dd06350cc9814b5a5dcf1fb9 Mon Sep 17 00:00:00 2001 From: HopeBaron Date: Mon, 12 Jul 2021 15:57:53 +0300 Subject: [PATCH 05/55] more work on this --- .../src/main/kotlin/entity/DiscordChannel.kt | 8 +-- .../behavior/channel/TextChannelBehavior.kt | 3 - ...adBehavior.kt => ThreadChannelBehavior.kt} | 11 ++-- .../threads/ThreadParentChannelBehavior.kt | 10 ++- .../thread/{Thread.kt => ThreadChannel.kt} | 4 +- .../kotlin/supplier/CacheEntitySupplier.kt | 62 +++++++++++++++++++ .../main/kotlin/supplier/EntitySupplier.kt | 13 ++-- .../kotlin/supplier/FallbackEntitySupplier.kt | 33 +++++++++- .../kotlin/supplier/RestEntitySupplier.kt | 58 +++++++++++++++++ .../kotlin/json/request/ChannelRequests.kt | 3 +- .../src/main/kotlin/service/ChannelService.kt | 4 +- 11 files changed, 182 insertions(+), 27 deletions(-) rename core/src/main/kotlin/behavior/channel/threads/{ThreadBehavior.kt => ThreadChannelBehavior.kt} (81%) rename core/src/main/kotlin/entity/channel/thread/{Thread.kt => ThreadChannel.kt} (79%) diff --git a/common/src/main/kotlin/entity/DiscordChannel.kt b/common/src/main/kotlin/entity/DiscordChannel.kt index 43b16638be0a..42352e5862fb 100644 --- a/common/src/main/kotlin/entity/DiscordChannel.kt +++ b/common/src/main/kotlin/entity/DiscordChannel.kt @@ -100,11 +100,11 @@ sealed class ChannelType(val value: Int) { /** A channel in which game developers can sell their game on Discord. */ object GuildStore : ChannelType(6) - object NewsThread : ChannelType(10) + object PublicNewsThread : ChannelType(10) object PrivateThread : ChannelType(11) - object PublicThread : ChannelType(12) + object PublicGuildThread : ChannelType(12) object GuildStageVoice : ChannelType(13) @@ -122,9 +122,9 @@ sealed class ChannelType(val value: Int) { 4 -> GuildCategory 5 -> GuildNews 6 -> GuildStore - 10 -> NewsThread + 10 -> PublicNewsThread 11 -> PrivateThread - 12 -> PublicThread + 12 -> PublicGuildThread 13 -> GuildStageVoice else -> Unknown(code) } diff --git a/core/src/main/kotlin/behavior/channel/TextChannelBehavior.kt b/core/src/main/kotlin/behavior/channel/TextChannelBehavior.kt index ee411a21b2d7..52ab3c73cc70 100644 --- a/core/src/main/kotlin/behavior/channel/TextChannelBehavior.kt +++ b/core/src/main/kotlin/behavior/channel/TextChannelBehavior.kt @@ -12,9 +12,6 @@ import dev.kord.core.supplier.EntitySupplyStrategy import dev.kord.rest.builder.channel.TextChannelModifyBuilder import dev.kord.rest.request.RestRequestException import dev.kord.rest.service.patchTextChannel -import dev.kord.core.entity.channel.thread.Thread -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow import java.util.* import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind diff --git a/core/src/main/kotlin/behavior/channel/threads/ThreadBehavior.kt b/core/src/main/kotlin/behavior/channel/threads/ThreadChannelBehavior.kt similarity index 81% rename from core/src/main/kotlin/behavior/channel/threads/ThreadBehavior.kt rename to core/src/main/kotlin/behavior/channel/threads/ThreadChannelBehavior.kt index 20bf83d7cf3c..c3f2738ee4ee 100644 --- a/core/src/main/kotlin/behavior/channel/threads/ThreadBehavior.kt +++ b/core/src/main/kotlin/behavior/channel/threads/ThreadChannelBehavior.kt @@ -2,9 +2,8 @@ package dev.kord.core.behavior.channel.threads import dev.kord.common.entity.Snowflake import dev.kord.core.behavior.channel.GuildChannelBehavior -import dev.kord.core.cache.data.ThreadUserData import dev.kord.core.cache.data.toData -import dev.kord.core.entity.channel.thread.Thread +import dev.kord.core.entity.channel.thread.ThreadChannel import dev.kord.core.entity.channel.thread.ThreadUser import dev.kord.rest.builder.channel.thread.ThreadModifyBuilder import kotlinx.coroutines.flow.Flow @@ -13,11 +12,12 @@ import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract -interface ThreadBehavior : GuildChannelBehavior { +interface ThreadChannelBehavior : GuildChannelBehavior { val members: Flow get() = flow { - val threadUsers = supplier.getThreadMembersOrNull(id) + supplier.getThreadMembers(id) + } suspend fun removeUser(userId: Snowflake) { kord.rest.channel.removeUserFromThread(id, userId) @@ -35,10 +35,11 @@ interface ThreadBehavior : GuildChannelBehavior { kord.rest.channel.leaveThread(id) } + } @OptIn(ExperimentalContracts::class) -suspend inline fun ThreadBehavior.edit(builder: ThreadModifyBuilder.() -> Unit): Thread { +suspend inline fun ThreadChannelBehavior.edit(builder: ThreadModifyBuilder.() -> Unit): ThreadChannel { contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } val appliedBuilder = ThreadModifyBuilder().apply(builder) val patchedChannel = kord.rest.channel.patchThread(id, appliedBuilder.toRequest(), appliedBuilder.reason) diff --git a/core/src/main/kotlin/behavior/channel/threads/ThreadParentChannelBehavior.kt b/core/src/main/kotlin/behavior/channel/threads/ThreadParentChannelBehavior.kt index 510c3d7659dd..2f9293979785 100644 --- a/core/src/main/kotlin/behavior/channel/threads/ThreadParentChannelBehavior.kt +++ b/core/src/main/kotlin/behavior/channel/threads/ThreadParentChannelBehavior.kt @@ -1,14 +1,18 @@ package dev.kord.core.behavior.channel.threads +import dev.kord.common.entity.Snowflake import dev.kord.core.behavior.channel.GuildChannelBehavior -import dev.kord.core.entity.channel.thread.Thread +import dev.kord.core.entity.channel.thread.ThreadChannel import kotlinx.coroutines.flow.Flow interface ThreadParentChannelBehavior : GuildChannelBehavior { - val publicActiveThreads: Flow get() = supplier.getActiveThreads(id) + val publicActiveThreads: Flow get() = supplier.getActiveThreads(id) + + fun getPublicArchivedThreads(messageId: Snowflake, limit: Int = Int.MAX_VALUE) { + + } - val publicArchievedThreads: Flow get() = supplier.getPublicArchivedThreads(id) } \ No newline at end of file diff --git a/core/src/main/kotlin/entity/channel/thread/Thread.kt b/core/src/main/kotlin/entity/channel/thread/ThreadChannel.kt similarity index 79% rename from core/src/main/kotlin/entity/channel/thread/Thread.kt rename to core/src/main/kotlin/entity/channel/thread/ThreadChannel.kt index c74046cee53e..253be3ae66bf 100644 --- a/core/src/main/kotlin/entity/channel/thread/Thread.kt +++ b/core/src/main/kotlin/entity/channel/thread/ThreadChannel.kt @@ -3,11 +3,11 @@ package dev.kord.core.entity.channel.thread import dev.kord.common.entity.Snowflake import dev.kord.common.entity.optional.value import dev.kord.core.Kord -import dev.kord.core.behavior.channel.threads.ThreadBehavior +import dev.kord.core.behavior.channel.threads.ThreadChannelBehavior import dev.kord.core.cache.data.ChannelData import dev.kord.core.supplier.EntitySupplier -class Thread(val data: ChannelData, override val kord: Kord, override val supplier: EntitySupplier) : ThreadBehavior { +class ThreadChannel(val data: ChannelData, override val kord: Kord, override val supplier: EntitySupplier = kord.defaultSupplier) : ThreadChannelBehavior { private val threadData = data.threadsMetadata.value!! diff --git a/core/src/main/kotlin/supplier/CacheEntitySupplier.kt b/core/src/main/kotlin/supplier/CacheEntitySupplier.kt index 351fd651fd3a..59b081ffd2a0 100644 --- a/core/src/main/kotlin/supplier/CacheEntitySupplier.kt +++ b/core/src/main/kotlin/supplier/CacheEntitySupplier.kt @@ -2,6 +2,7 @@ package dev.kord.core.supplier import dev.kord.cache.api.DataCache import dev.kord.cache.api.query +import dev.kord.common.entity.ChannelType import dev.kord.common.entity.Snowflake import dev.kord.common.exception.RequestException @@ -13,10 +14,14 @@ import dev.kord.core.cache.idGt import dev.kord.core.entity.* import dev.kord.core.entity.channel.Channel import dev.kord.core.entity.channel.GuildChannel +import dev.kord.core.entity.channel.thread.ThreadChannel +import dev.kord.core.entity.channel.thread.ThreadUser import dev.kord.core.exception.EntityNotFoundException import dev.kord.gateway.Gateway import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.* +import kotlinx.datetime.Instant +import kotlinx.datetime.toInstant /** * [EntitySupplier] that uses a [DataCache] to resolve entities. @@ -274,6 +279,63 @@ class CacheEntitySupplier(private val kord: Kord) : EntitySupplier { override suspend fun getStageInstanceOrNull(channelId: Snowflake): StageInstance? = null + override fun getThreadMembers(channelId: Snowflake): Flow { + return cache.query { + idEq(ThreadUserData::id, channelId) + }.asFlow().map { ThreadUser(it, kord) } + } + + override fun getActiveThreads(channelId: Snowflake): Flow { + return cache.query { + idEq(ChannelData::id, channelId) + }.asFlow().filter { + it.threadsMetadata.value?.archived != true + }.map { + ThreadChannel(it, kord) + } + } + + override fun getPublicArchivedThreads(channelId: Snowflake, before: Instant, limit: Int): Flow { + return cache.query { + idEq(ChannelData::id, channelId) + }.asFlow().filter { + val time = it.threadsMetadata.value?.archiveTimestamp?.toInstant() + it.threadsMetadata.value?.archived != true + && time != null + && time < before + && (it.type == ChannelType.PublicGuildThread || it.type == ChannelType.PublicNewsThread) + }.take(limit).map { ThreadChannel(it, kord) } + } + + override fun getPrivateArchivedThreads(channelId: Snowflake, before: Instant, limit: Int): Flow { + return cache.query { + idEq(ChannelData::id, channelId) + }.asFlow().filter { + val time = it.threadsMetadata.value?.archiveTimestamp?.toInstant() + it.threadsMetadata.value?.archived == true + && time != null + && time < before + && it.type == ChannelType.PrivateThread + }.take(limit).map { ThreadChannel(it, kord) } + } + + override fun getJoinedPrivateArchivedThreads( + channelId: Snowflake, + before: Instant, + limit: Int + ): Flow { + return cache.query { + idEq(ChannelData::id, channelId) + }.asFlow().filter { + val time = it.threadsMetadata.value?.archiveTimestamp?.toInstant() + it.threadsMetadata.value?.archived == true + && time != null + && time < before + && it.type == ChannelType.PrivateThread + && kord.selfId in getThreadMembers(channelId).map { user -> user.data.id }.toList() + }.take(limit).map { ThreadChannel(it, kord) } + } + 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 63470dcbad12..6a9ae262ffb6 100644 --- a/core/src/main/kotlin/supplier/EntitySupplier.kt +++ b/core/src/main/kotlin/supplier/EntitySupplier.kt @@ -7,10 +7,11 @@ import dev.kord.core.entity.* import dev.kord.core.entity.channel.Channel import dev.kord.core.entity.channel.GuildChannel import dev.kord.core.entity.channel.MessageChannel -import dev.kord.core.entity.channel.thread.Thread +import dev.kord.core.entity.channel.thread.ThreadChannel import dev.kord.core.entity.channel.thread.ThreadUser import dev.kord.core.exception.EntityNotFoundException import kotlinx.coroutines.flow.Flow +import kotlinx.datetime.Instant /** * An abstraction that allows for requesting Discord entities. @@ -397,15 +398,15 @@ interface EntitySupplier { suspend fun getStageInstance(channelId: Snowflake): StageInstance = getStageInstanceOrNull(channelId) ?: EntityNotFoundException.stageInstanceNotFound(channelId) - fun getThreadMembersOrNull(channelId: Snowflake): Flow + fun getThreadMembers(channelId: Snowflake): Flow - fun getActiveThreads(channelId: Snowflake): Flow + fun getActiveThreads(channelId: Snowflake): Flow - fun getPublicArchivedThreads(channelId: Snowflake): Flow + fun getPublicArchivedThreads(channelId: Snowflake, before: Instant, limit: Int): Flow - fun getPrivateArchivedThreads(channelId: Snowflake): Flow + fun getPrivateArchivedThreads(channelId: Snowflake, before: Instant, limit: Int): Flow - fun getJoinedPrivateArchivedThreads(channelId: Snowflake): Flow + fun getJoinedPrivateArchivedThreads(channelId: Snowflake, before: Instant, limit: Int): Flow } diff --git a/core/src/main/kotlin/supplier/FallbackEntitySupplier.kt b/core/src/main/kotlin/supplier/FallbackEntitySupplier.kt index f352cf14de07..9f4ddda5e4a0 100644 --- a/core/src/main/kotlin/supplier/FallbackEntitySupplier.kt +++ b/core/src/main/kotlin/supplier/FallbackEntitySupplier.kt @@ -4,8 +4,11 @@ import dev.kord.common.entity.Snowflake import dev.kord.core.entity.* import dev.kord.core.entity.channel.Channel import dev.kord.core.entity.channel.GuildChannel +import dev.kord.core.entity.channel.thread.ThreadChannel +import dev.kord.core.entity.channel.thread.ThreadUser import dev.kord.core.switchIfEmpty import kotlinx.coroutines.flow.Flow +import kotlinx.datetime.Instant /** * Creates supplier providing a strategy which will first operate on this supplier. When an entity @@ -114,7 +117,35 @@ private class FallbackEntitySupplier(val first: EntitySupplier, val second: Enti override fun getTemplates(guildId: Snowflake): Flow