Skip to content

Commit

Permalink
Implement Threads (#349)
Browse files Browse the repository at this point in the history
* thread gateway/rest implementation

* add service endpoints and channel requests

* split invite from categorizables

* Add threads, thread users, missing routes

There's still more modifications to do but hopefully we are close to a milestone in this regards.

* more work on this

* core events

* Almost done (behavior/event handling)

* Fix compilation errors

* Consider channels with unknown but same structure

* implement thread parents behavior into news and text channels

* implement missing store functions and enable v9 for gateway

* documentation

* fix core functions and names

* rename publicActiveThreads to activeThreads

* remove list prefix from routes

* Fix json incorrect and missing fields

* Fix incorrect jsons

* make a thread modify request a part of channel modify request

* remodel thread user

* remodel thread user & add missing properties/functions

* proper startXThread support

* move startPublicThreadWithMessage to thread parents

* Cleanup

* narrow down the thread channel types

* add rest tests

* fix rest tests

* fix user thread data conversion

* add description

* register thread user data

* add missing behaviors and clean up code

* Add missing getter functions

* fix withStrategy signiture

* remove has_more from ListThreadResponse

* paginate thread suppliers

* Fix errors and correct method signitures

All tests passed

* optimize imports

* add ThreadParentChannel and cleanup its behavior

* add core sync/updates events

* handle thread events (broken)

* cleanup ChannelData

* handle unknown channel type

* move thread related events under the same package

* correct typo

* remove thread events from the channel handlers

* clean up and document threads

* further documentation

* further documentation (2)

* Fix markdowns

* rename the mis-leading thread-starting functions

* Fix cache querying for threads

* invert boolean check for public archived threads

* apply suggestions

* provide default values for durations

* provide unsafe thread parent behaviors

* support X-Audit-Log
  • Loading branch information
HopeBaron authored Jul 28, 2021
1 parent df9ad86 commit f0d32be
Show file tree
Hide file tree
Showing 58 changed files with 1,971 additions and 45 deletions.
65 changes: 64 additions & 1 deletion common/src/main/kotlin/entity/DiscordChannel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,16 @@ data class DiscordChannel(
val parentId: OptionalSnowflake? = OptionalSnowflake.Missing,
@SerialName("last_pin_timestamp")
val lastPinTimestamp: Optional<String?> = Optional.Missing(),
val permissions: Optional<Permissions> = Optional.Missing()
val permissions: Optional<Permissions> = Optional.Missing(),
@SerialName("message_count")
val messageCount: OptionalInt = OptionalInt.Missing,
@SerialName("member_count")
val memberCount: OptionalInt = OptionalInt.Missing,
@SerialName("thread_metadata")
val threadMetadata: Optional<DiscordThreadMetadata> = Optional.Missing(),
@SerialName("default_auto_archive_duration")
val defaultAutoArchiveDuration: Optional<ArchiveDuration> = Optional.Missing(),
val member: Optional<DiscordThreadMember> = Optional.Missing()
)

@Serializable(with = ChannelType.Serializer::class)
Expand Down Expand Up @@ -94,6 +103,14 @@ sealed class ChannelType(val value: Int) {
/** A channel in which game developers can sell their game on Discord. */
object GuildStore : ChannelType(6)

object PublicNewsThread : ChannelType(10)

object PublicGuildThread : ChannelType(11)

object PrivateThread : ChannelType(12)



object GuildStageVoice : ChannelType(13)

companion object;
Expand All @@ -110,6 +127,9 @@ sealed class ChannelType(val value: Int) {
4 -> GuildCategory
5 -> GuildNews
6 -> GuildStore
10 -> PublicNewsThread
11 -> PublicGuildThread
12 -> PrivateThread
13 -> GuildStageVoice
else -> Unknown(code)
}
Expand Down Expand Up @@ -154,3 +174,46 @@ sealed class OverwriteType(val value: Int) {
}
}
}

@Serializable
class DiscordThreadMetadata(
val archived: Boolean,
@SerialName("archive_timestamp")
val archiveTimestamp: String,
@SerialName("auto_archive_duration")
val autoArchiveDuration: ArchiveDuration,
val locked: OptionalBoolean = OptionalBoolean.Missing
)

@Serializable(with = ArchiveDuration.Serializer::class)
sealed class ArchiveDuration(val duration: Int) {
class Unknown(duration: Int) : ArchiveDuration(duration)
object Hour : ArchiveDuration(60)
object Day : ArchiveDuration(1440)
object ThreeDays : ArchiveDuration(4320)
object Week : ArchiveDuration(10080)

object Serializer : KSerializer<ArchiveDuration> {
override fun deserialize(decoder: Decoder): ArchiveDuration {
val value = decoder.decodeInt()
return values.firstOrNull { it.duration == value } ?: Unknown(value)
}

override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor("AutoArchieveDuration", PrimitiveKind.INT)

override fun serialize(encoder: Encoder, value: ArchiveDuration) {
encoder.encodeInt(value.duration)
}
}

companion object {
val values: Set<ArchiveDuration>
get() = setOf(
Hour,
Day,
ThreeDays,
Week,
)
}
}
1 change: 1 addition & 0 deletions common/src/main/kotlin/entity/DiscordGuild.kt
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ data class DiscordGuild(
val voiceStates: Optional<List<DiscordVoiceState>> = Optional.Missing(),
val members: Optional<List<DiscordGuildMember>> = Optional.Missing(),
val channels: Optional<List<DiscordChannel>> = Optional.Missing(),
val threads: Optional<List<DiscordChannel>> = Optional.Missing(),
val presences: Optional<List<DiscordPresenceUpdate>> = Optional.Missing(),
@SerialName("max_presences")
val maxPresences: OptionalInt? = OptionalInt.Missing,
Expand Down
9 changes: 7 additions & 2 deletions common/src/main/kotlin/entity/DiscordMessage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ data class DiscordMessage(
* This is a list even though the docs say it's a component
*/
val components: Optional<List<DiscordComponent>> = Optional.Missing(),
val interaction: Optional<DiscordMessageInteraction> = Optional.Missing()
val interaction: Optional<DiscordMessageInteraction> = Optional.Missing(),
val thread: Optional<DiscordChannel> = Optional.Missing()
)

/**
Expand Down Expand Up @@ -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)
Expand Down
11 changes: 11 additions & 0 deletions common/src/main/kotlin/entity/Member.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package dev.kord.common.entity

import dev.kord.common.entity.optional.Optional
import dev.kord.common.entity.optional.OptionalBoolean
import dev.kord.common.entity.optional.OptionalSnowflake
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

Expand Down Expand Up @@ -82,3 +83,13 @@ data class DiscordUpdatedGuildMember(
val premiumSince: Optional<String?> = Optional.Missing(),
val pending: OptionalBoolean = OptionalBoolean.Missing
)

@Serializable
data class DiscordThreadMember(
val id: OptionalSnowflake = OptionalSnowflake.Missing,
@SerialName("user_id")
val userId: OptionalSnowflake = OptionalSnowflake.Missing,
@SerialName("join_timestamp")
val joinTimestamp: String,
val flags: Int
)
8 changes: 7 additions & 1 deletion common/src/main/kotlin/entity/Permission.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -192,7 +195,10 @@ sealed class Permission(val code: DiscordBitSet) {
ManageWebhooks,
ManageEmojis,
UseSlashCommands,
RequestToSpeak
RequestToSpeak,
ManageThreads,
UsePublicThreads,
UsePrivateThreads,
)
}
}
18 changes: 18 additions & 0 deletions core/src/main/kotlin/Unsafe.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ 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.core.behavior.channel.threads.PrivateThreadParentChannelBehavior
import dev.kord.core.behavior.channel.threads.ThreadChannelBehavior
import dev.kord.core.behavior.channel.threads.ThreadParentChannelBehavior
import dev.kord.core.behavior.interaction.ComponentInteractionBehavior
import dev.kord.rest.service.InteractionService

Expand Down Expand Up @@ -50,6 +53,18 @@ class Unsafe(private val kord: Kord) {
fun storeChannel(guildId: Snowflake, id: Snowflake): StoreChannelBehavior =
StoreChannelBehavior(guildId = guildId, id = id, kord = kord)


fun publicThreadParent(guildId: Snowflake, id: Snowflake): ThreadParentChannelBehavior =
ThreadParentChannelBehavior(guildId, id, kord)


fun privateThreadParent(guildId: Snowflake, id: Snowflake): PrivateThreadParentChannelBehavior =
PrivateThreadParentChannelBehavior(guildId, id, kord)

fun thread(id: Snowflake): ThreadChannelBehavior =
ThreadChannelBehavior(id, kord)


fun guild(id: Snowflake): GuildBehavior =
GuildBehavior(id, kord)

Expand All @@ -62,6 +77,9 @@ class Unsafe(private val kord: Kord) {
fun user(id: Snowflake): UserBehavior =
UserBehavior(id, kord)

fun threadUser(id: Snowflake, threadId: Snowflake) =
ThreadUserBehavior(id, threadId, kord)

fun member(guildId: Snowflake, id: Snowflake): MemberBehavior =
MemberBehavior(guildId = guildId, id = id, kord = kord)

Expand Down
50 changes: 47 additions & 3 deletions core/src/main/kotlin/Util.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ package dev.kord.core
import dev.kord.common.entity.Snowflake
import dev.kord.core.entity.Entity
import dev.kord.core.entity.KordEntity
import dev.kord.core.entity.channel.thread.ThreadChannel
import dev.kord.core.event.Event
import dev.kord.core.event.user.PresenceUpdateEvent
import dev.kord.core.event.user.VoiceStateUpdateEvent
import dev.kord.core.event.guild.WebhookUpdateEvent
import dev.kord.core.event.channel.*
import dev.kord.core.event.guild.*
import dev.kord.core.event.message.*
import dev.kord.core.event.role.RoleCreateEvent
import dev.kord.core.event.role.RoleDeleteEvent
import dev.kord.core.event.role.RoleUpdateEvent
import dev.kord.core.event.user.PresenceUpdateEvent
import dev.kord.core.event.user.VoiceStateUpdateEvent
import dev.kord.gateway.Intent.*
import dev.kord.gateway.Intents
import dev.kord.gateway.MessageDelete
Expand All @@ -21,6 +21,7 @@ import dev.kord.rest.json.JsonErrorCode
import dev.kord.rest.request.RestRequestException
import dev.kord.rest.route.Position
import kotlinx.coroutines.flow.*
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
Expand Down Expand Up @@ -231,6 +232,49 @@ internal fun <C : Collection<T>, T : KordEntity> paginateBackwards(
): Flow<T> =
paginate(start, batchSize, { it.id }, oldestItem { it.id }, Position::Before, request)

/**
* Paginates the [Collection] returned by [request] with [start] as a initial reference in time.
* [instantSelector] is used to select the new reference to fetch from.
*
* Termination scenarios:
* * [Collection]'s size fall behind [batchSize].
* * [instantSelector] returns null.
*/
internal fun <C : Collection<T>, T> paginateByDate(
start: Instant = Clock.System.now(),
batchSize: Int,
instantSelector: (Collection<T>) -> Instant?,
request: suspend (Instant) -> C
): Flow<T> = flow {

var currentTimestamp = start
while (true) {
val response = request(currentTimestamp)

for (item in response) emit(item)

currentTimestamp = instantSelector(response) ?: break
if (response.size < batchSize) break
}
}

/**
* A special function to paginate [ThreadChannel] endpoints.
* selects the earliest time reference found in the response of the request on each pagination.
* see [paginateByDate]
*/
internal fun paginateThreads(
batchSize: Int,
start: Instant = Clock.System.now(),
request: suspend (Instant) -> Collection<ThreadChannel>
) =
paginateByDate(
start,
batchSize,
{ threads -> threads.minOfOrNull { it.archiveTimeStamp } },
request
)

inline fun <reified T : Event> Intents.IntentsBuilder.enableEvent() = enableEvent(T::class)

fun Intents.IntentsBuilder.enableEvents(events: Iterable<KClass<out Event>>) = events.forEach { enableEvent(it) }
Expand Down
44 changes: 44 additions & 0 deletions core/src/main/kotlin/behavior/ThreadUserBehavior.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package dev.kord.core.behavior

import dev.kord.common.entity.Snowflake
import dev.kord.core.Kord
import dev.kord.core.behavior.channel.threads.ThreadChannelBehavior
import dev.kord.core.entity.channel.thread.ThreadChannel
import dev.kord.core.supplier.EntitySupplier
import dev.kord.core.supplier.EntitySupplyStrategy
import dev.kord.core.supplier.getChannelOf
import dev.kord.core.supplier.getChannelOfOrNull

interface ThreadUserBehavior : UserBehavior {

val threadId: Snowflake

val thread: ThreadChannelBehavior get() = ThreadChannelBehavior(threadId, kord)

suspend fun getThread(): ThreadChannel = supplier.getChannelOf(threadId)

suspend fun getThreadOrNull(): ThreadChannel? = supplier.getChannelOfOrNull(threadId)

override fun withStrategy(strategy: EntitySupplyStrategy<*>): UserBehavior {
return ThreadUserBehavior(id, threadId, kord, strategy.supply(kord))

}
}

fun ThreadUserBehavior(
id: Snowflake,
threadId: Snowflake,
kord: Kord,
supplier: EntitySupplier = kord.defaultSupplier
): ThreadUserBehavior {
return object : ThreadUserBehavior {
override val id: Snowflake
get() = id
override val threadId: Snowflake
get() = threadId
override val kord: Kord
get() = kord
override val supplier: EntitySupplier
get() = supplier
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import dev.kord.core.Kord
import dev.kord.core.cache.data.MessageData
import dev.kord.core.entity.Message
import dev.kord.core.entity.Strategizable
import dev.kord.core.entity.channel.GuildChannel
import dev.kord.core.entity.channel.MessageChannel
import dev.kord.core.exception.EntityNotFoundException
import dev.kord.core.supplier.EntitySupplier
Expand Down
Loading

0 comments on commit f0d32be

Please sign in to comment.