commonMain.Util.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of kord-core Show documentation
Show all versions of kord-core Show documentation
Idiomatic Kotlin Wrapper for The Discord API
package dev.kord.core
import dev.kord.common.annotation.KordInternal
import dev.kord.common.entity.Snowflake
import dev.kord.core.entity.Message
import dev.kord.core.entity.channel.thread.ThreadChannel
import dev.kord.core.event.Event
import dev.kord.core.event.automoderation.*
import dev.kord.core.event.channel.*
import dev.kord.core.event.channel.thread.*
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.Intent.*
import dev.kord.gateway.Intents
import dev.kord.gateway.PrivilegedIntent
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.InvocationKind
import kotlin.contracts.contract
import kotlin.reflect.KClass
internal inline fun catchNotFound(block: () -> T): T? {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return try {
block()
} catch (exception: RestRequestException) {
if (exception.status.code == 404) null
else throw exception
}
}
internal inline fun catchDiscordError(vararg codes: JsonErrorCode, block: () -> T): T? {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return try {
block()
} catch (exception: RestRequestException) {
when {
codes.isEmpty() -> null
exception.error?.code in codes -> null
else -> throw exception
}
}
}
internal fun > Flow.sorted(): Flow = flow {
toList().sorted().forEach { emit(it) }
}
/**
* The terminal operator that returns `true` if any of the elements match [predicate].
* The flow's collection is cancelled when a match is found.
*/
internal suspend inline fun Flow.any(crossinline predicate: suspend (T) -> Boolean): Boolean =
firstOrNull { predicate(it) } != null
/**
* The non-terminal operator that returns a new flow that will emit values of the second [flow] only after the first
* flow finished collecting without values.
*
* ```kotlin
* emptyFlow().switchIfEmpty(flowOf("hello", "world")) //["hello", "world"]
*
* flowOf("hello", "world").switchIfEmpty(flowOf("goodbye", "world")) //["hello", "world"]
* ```
*/
internal fun Flow.switchIfEmpty(flow: Flow): Flow = onEmpty {
emitAll(flow)
}
/**
* The terminal operator that returns the index of the first element emitted by the flow that matches the [predicate]
* and then cancels flow's collection.
* Returns `null` if the flow was empty or no element matched the [predicate].
*/
internal suspend fun Flow.indexOfFirstOrNull(predicate: suspend (T) -> Boolean): Int? {
var counter = 0
return map {
val pair = counter to it
counter += 1
pair
}
.filter { predicate(it.second) }
.take(1)
.singleOrNull()?.first
}
internal fun , Item : Any, Direction : Position.BeforeOrAfter> paginate(
start: Snowflake,
batchSize: Int,
itemSelector: (Batch) -> Item?,
idSelector: (Item) -> Snowflake,
directionSelector: (Snowflake) -> Direction,
request: suspend (Direction) -> Batch,
): Flow- = flow {
var direction = directionSelector(start)
while (true) {
val batch = request(direction)
for (item in batch) emit(item)
if (batch.size < batchSize) break
val item = itemSelector(batch) ?: break
direction = directionSelector(idSelector(item))
}
}
/**
* Discord returns values in order newest -> oldest (big -> small) (confirmed for messages),
* meaning that the first item returned is the one last created (youngest) in the batch.
*/
internal fun
youngestItem(idSelector: (T) -> Snowflake): (Collection) -> T? = function@{
if (it.size <= 1) return@function it.firstOrNull()
val first = it.first()
val last = it.last()
val firstId = idSelector(first).value
val lastId = idSelector(last).value
if (firstId > lastId) first
else last
}
/**
* Discord returns values in order oldest -> newest (big -> small) (confirmed for messages),
* meaning that the last item returned is the one first created (oldest) in the batch.
*/
internal fun oldestItem(idSelector: (T) -> Snowflake): (Collection) -> T? = function@{
if (it.size <= 1) return@function it.firstOrNull()
val first = it.first()
val last = it.last()
val firstId = idSelector(first).value
val lastId = idSelector(last).value
if (firstId < lastId) first
else last
}
/**
* Selects the [Position.After] the youngest item in the batch.
*/
internal fun paginateForwards(
batchSize: Int,
start: Snowflake = Snowflake.min,
idSelector: (T) -> Snowflake,
request: suspend (after: Position.After) -> Collection,
): Flow = paginate(
start,
batchSize,
itemSelector = youngestItem(idSelector),
idSelector,
directionSelector = Position::After,
request,
)
/**
* Selects the [Position.Before] the oldest item in the batch.
*/
internal fun paginateBackwards(
batchSize: Int,
start: Snowflake = Snowflake.max,
idSelector: (T) -> Snowflake,
request: suspend (before: Position.Before) -> Collection,
): Flow = paginate(
start,
batchSize,
itemSelector = oldestItem(idSelector),
idSelector,
directionSelector = Position::Before,
request,
)
/**
* Paginates the [Collection] returned by [request] with [start] as an 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 , Item : Any> paginateByDate(
batchSize: Int,
start: Instant?,
instantSelector: (Batch) -> Instant?,
request: suspend (Instant) -> Batch,
): Flow- = flow {
var currentTimestamp = start ?: Clock.System.now() // get default current time as late as possible
while (true) {
val batch = request(currentTimestamp)
for (item in batch) emit(item)
if (batch.size < batchSize) break
currentTimestamp = instantSelector(batch) ?: break
}
}
/**
* A special function to paginate [ThreadChannel] endpoints.
* selects the earliest reference in time found in the response of the request on each pagination.
* see [paginateByDate]
*/
internal fun paginateThreads(
batchSize: Int,
start: Instant?,
request: suspend (Instant) -> Collection
,
) = paginateByDate(
batchSize,
start,
instantSelector = { threads -> threads.minOfOrNull { it.archiveTimestamp } },
request,
)
/**
* Adds the necessary [Intent]s to receive the specified type of event in all variations and with all data available.
*
* E.g. [MessageCreateEvent] will add the [GuildMessages], [DirectMessages] and [MessageContent] intents to receive
* messages in Guilds and DMs with the full [content][Message.content].
*
* Note that enabling one type of event might also enable several other types of events since most [Intent]s enable more
* than one event.
*/
public inline fun Intents.Builder.enableEvent(): Unit = enableEvent(T::class)
@Suppress("DEPRECATION_ERROR")
@Deprecated("'Intents.IntentsBuilder' is deprecated, use 'Intents.Builder' instead.", level = DeprecationLevel.HIDDEN)
public inline fun Intents.IntentsBuilder.enableEvent(): Unit = enableEvent0(T::class)
/**
* Adds the necessary [Intent]s to receive the specified types of [events] in all variations and with all data
* available.
*
* E.g. [MessageCreateEvent] will add the [GuildMessages], [DirectMessages] and [MessageContent] intents to receive
* messages in Guilds and DMs with the full [content][Message.content].
*
* Note that enabling one type of event might also enable several other types of events since most [Intent]s enable more
* than one event.
*/
public fun Intents.Builder.enableEvents(events: Iterable>): Unit =
events.forEach { enableEvent(it) }
@Suppress("DEPRECATION_ERROR")
@Deprecated("'Intents.IntentsBuilder' is deprecated, use 'Intents.Builder' instead.", level = DeprecationLevel.HIDDEN)
public fun Intents.IntentsBuilder.enableEvents(events: Iterable>): Unit =
events.forEach { enableEvent0(it) }
/**
* Adds the necessary [Intent]s to receive the specified types of [events] in all variations and with all data
* available.
*
* E.g. [MessageCreateEvent] will add the [GuildMessages], [DirectMessages] and [MessageContent] intents to receive
* messages in Guilds and DMs with the full [content][Message.content].
*
* Note that enabling one type of event might also enable several other types of events since most [Intent]s enable more
* than one event.
*/
public fun Intents.Builder.enableEvents(vararg events: KClass): Unit =
events.forEach { enableEvent(it) }
@Suppress("DEPRECATION_ERROR")
@Deprecated("'Intents.IntentsBuilder' is deprecated, use 'Intents.Builder' instead.", level = DeprecationLevel.HIDDEN)
public fun Intents.IntentsBuilder.enableEvents(vararg events: KClass): Unit =
events.forEach { enableEvent0(it) }
/**
* Adds the necessary [Intent]s to receive the specified type of [event] in all variations and with all data available.
*
* E.g. [MessageCreateEvent] will add the [GuildMessages], [DirectMessages] and [MessageContent] intents to receive
* messages in Guilds and DMs with the full [content][Message.content].
*
* Note that enabling one type of event might also enable several other types of events since most [Intent]s enable more
* than one event.
*/
@OptIn(PrivilegedIntent::class)
public fun Intents.Builder.enableEvent(event: KClass): Unit = when (event) {
// see https://discord.com/developers/docs/topics/gateway#list-of-intents
/*
* events requiring a single intent:
*/
GuildCreateEvent::class,
GuildUpdateEvent::class,
GuildDeleteEvent::class,
RoleCreateEvent::class,
RoleUpdateEvent::class,
RoleDeleteEvent::class,
ChannelCreateEvent::class,
CategoryCreateEvent::class,
DMChannelCreateEvent::class,
NewsChannelCreateEvent::class,
StageChannelCreateEvent::class,
TextChannelCreateEvent::class,
ForumChannelCreateEvent::class,
MediaChannelCreateEvent::class,
UnknownChannelCreateEvent::class,
VoiceChannelCreateEvent::class,
ChannelUpdateEvent::class,
CategoryUpdateEvent::class,
DMChannelUpdateEvent::class,
NewsChannelUpdateEvent::class,
StageChannelUpdateEvent::class,
TextChannelUpdateEvent::class,
ForumChannelUpdateEvent::class,
MediaChannelUpdateEvent::class,
UnknownChannelUpdateEvent::class,
VoiceChannelUpdateEvent::class,
ChannelDeleteEvent::class,
CategoryDeleteEvent::class,
DMChannelDeleteEvent::class,
NewsChannelDeleteEvent::class,
StageChannelDeleteEvent::class,
TextChannelDeleteEvent::class,
ForumChannelDeleteEvent::class,
MediaChannelDeleteEvent::class,
UnknownChannelDeleteEvent::class,
VoiceChannelDeleteEvent::class,
ThreadChannelCreateEvent::class,
NewsChannelThreadCreateEvent::class,
TextChannelThreadCreateEvent::class,
UnknownChannelThreadCreateEvent::class,
ThreadUpdateEvent::class,
NewsChannelThreadUpdateEvent::class,
TextChannelThreadUpdateEvent::class,
UnknownChannelThreadUpdateEvent::class,
ThreadChannelDeleteEvent::class,
NewsChannelThreadDeleteEvent::class,
TextChannelThreadDeleteEvent::class,
UnknownChannelThreadDeleteEvent::class,
ThreadListSyncEvent::class,
ThreadMemberUpdateEvent::class,
-> +Guilds
MemberJoinEvent::class, MemberUpdateEvent::class, MemberLeaveEvent::class -> +GuildMembers
GuildAuditLogEntryCreateEvent::class, BanAddEvent::class, BanRemoveEvent::class -> +GuildModeration
EmojisUpdateEvent::class -> +GuildEmojis
IntegrationsUpdateEvent::class, IntegrationCreateEvent::class, IntegrationUpdateEvent::class, IntegrationDeleteEvent::class -> +GuildIntegrations
WebhookUpdateEvent::class -> +GuildWebhooks
InviteCreateEvent::class, InviteDeleteEvent::class -> +GuildInvites
VoiceStateUpdateEvent::class -> +GuildVoiceStates
PresenceUpdateEvent::class -> +GuildPresences
MessageBulkDeleteEvent::class -> +GuildMessages // no message content
GuildScheduledEventEvent::class,
GuildScheduledEventCreateEvent::class,
GuildScheduledEventUpdateEvent::class,
GuildScheduledEventDeleteEvent::class,
GuildScheduledEventUserEvent::class,
GuildScheduledEventUserAddEvent::class,
GuildScheduledEventUserRemoveEvent::class,
-> +GuildScheduledEvents
AutoModerationRuleConfigurationEvent::class,
AutoModerationRuleCreateEvent::class,
AutoModerationRuleUpdateEvent::class,
AutoModerationRuleDeleteEvent::class,
-> +AutoModerationConfiguration
/*
* events requiring multiple intents:
*/
ChannelPinsUpdateEvent::class -> {
+Guilds
+DirectMessages
}
ThreadMembersUpdateEvent::class -> {
+Guilds
+GuildMembers
}
MessageCreateEvent::class, MessageUpdateEvent::class -> {
+GuildMessages
+DirectMessages
+MessageContent
}
MessageDeleteEvent::class -> {
+GuildMessages
+DirectMessages
// no message content
}
ReactionAddEvent::class, ReactionRemoveEvent::class, ReactionRemoveAllEvent::class, ReactionRemoveEmojiEvent::class -> {
+GuildMessageReactions
+DirectMessagesReactions
}
TypingStartEvent::class -> {
+GuildMessageTyping
+DirectMessageTyping
}
AutoModerationActionExecutionEvent::class -> {
+AutoModerationExecution
+MessageContent
}
AutoModerationEvent::class -> {
// supertype of AutoModerationRuleConfigurationEvent and AutoModerationActionExecutionEvent
// -> requires intents for both
+AutoModerationConfiguration
+AutoModerationExecution
+MessageContent
}
else -> Unit
}
@Suppress("DEPRECATION_ERROR")
@Deprecated("'Intents.IntentsBuilder' is deprecated, use 'Intents.Builder' instead.", level = DeprecationLevel.HIDDEN)
public fun Intents.IntentsBuilder.enableEvent(event: KClass) {
enableEvent0(event)
}
@Suppress("DEPRECATION_ERROR")
@PublishedApi
@KordInternal
internal fun Intents.IntentsBuilder.enableEvent0(event: KClass) {
+(Intents.Builder(flags().code).apply { enableEvent(event) }.build())
}
// Replacement of Objects.hash
internal fun hash(vararg values: Any?) = values.contentHashCode()