All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.supplier.RestEntitySupplier.kt Maven / Gradle / Ivy

The newest version!
package dev.kord.core.supplier

import dev.kord.common.entity.DiscordAuditLogEntry
import dev.kord.common.entity.DiscordEntitlement
import dev.kord.common.entity.DiscordSubscription
import dev.kord.common.entity.Snowflake
import dev.kord.common.entity.optional.OptionalSnowflake
import dev.kord.common.entity.optional.optionalSnowflake
import dev.kord.core.*
import dev.kord.core.cache.data.*
import dev.kord.core.entity.*
import dev.kord.core.entity.application.ApplicationCommandPermissions
import dev.kord.core.entity.application.GlobalApplicationCommand
import dev.kord.core.entity.application.GuildApplicationCommand
import dev.kord.core.entity.automoderation.AutoModerationRule
import dev.kord.core.entity.channel.Channel
import dev.kord.core.entity.channel.TopGuildChannel
import dev.kord.core.entity.channel.thread.ThreadChannel
import dev.kord.core.entity.channel.thread.ThreadMember
import dev.kord.core.entity.interaction.followup.FollowupMessage
import dev.kord.core.entity.monetization.Entitlement
import dev.kord.core.entity.monetization.Subscription
import dev.kord.core.exception.EntityNotFoundException
import dev.kord.rest.builder.auditlog.AuditLogGetRequestBuilder
import dev.kord.rest.json.request.*
import dev.kord.rest.request.RestRequestException
import dev.kord.rest.route.Position
import dev.kord.rest.service.RestClient
import kotlinx.coroutines.flow.*
import kotlinx.datetime.Instant
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.math.min

/**
 * [EntitySupplier] that uses a [RestClient] to resolve entities.
 *
 * Error codes besides 429(Too Many Requests) will throw a [RestRequestException],
 * 404(Not Found) will be caught by the `xOrNull` variant and return null instead.
 *
 * This supplier will always be able to resolve entities if they exist according
 * to Discord, entities will always be up-to-date at the moment of the call.
 */
public class RestEntitySupplier(public val kord: Kord) : EntitySupplier {

    // order like in docs:

    // interactions
    private inline val interaction get() = kord.rest.interaction

    // resources
    private inline val application get() = kord.rest.application
    private inline val auditLog get() = kord.rest.auditLog
    private inline val autoModeration get() = kord.rest.autoModeration
    private inline val channel get() = kord.rest.channel
    private inline val emoji get() = kord.rest.emoji
    private inline val entitlement get() = kord.rest.entitlement
    private inline val guild get() = kord.rest.guild
    private inline val template get() = kord.rest.template
    private inline val invite get() = kord.rest.invite
    private inline val stageInstance get() = kord.rest.stageInstance
    private inline val sticker get() = kord.rest.sticker
    private inline val subscription get() = kord.rest.subscription
    private inline val user get() = kord.rest.user
    private inline val voice get() = kord.rest.voice
    private inline val webhook get() = kord.rest.webhook


    // max batchSize/limit: see https://discord.com/developers/docs/resources/user#get-current-user-guilds
    override val guilds: Flow
        get() = paginateForwards(batchSize = 200, idSelector = { it.id }) { after ->
            user.getCurrentUserGuilds(position = after, limit = 200)
        }.map {
            val data = guild.getGuild(it.id).toData()
            Guild(data, kord)
        }

    override val regions: Flow
        get() = flow {
            voice.getVoiceRegions().forEach {
                val data = RegionData.from(OptionalSnowflake.Missing, it)
                emit(Region(data, kord))
            }
        }

    override suspend fun getChannelOrNull(id: Snowflake): Channel? =
        catchNotFound { Channel.from(channel.getChannel(id).toData(), kord) }

    override fun getGuildChannels(guildId: Snowflake): Flow = flow {
        for (channelData in guild.getGuildChannels(guildId))
            emit(Channel.from(ChannelData.from(channelData), kord))
    }.filterIsInstance()

    override fun getChannelPins(channelId: Snowflake): Flow = flow {
        for (messageData in channel.getChannelPins(channelId))
            emit(Message(MessageData.from(messageData), kord))
    }

    override suspend fun getGuildOrNull(id: Snowflake): Guild? =
        catchNotFound { Guild(guild.getGuild(id).toData(), kord) }

    override suspend fun getGuildPreviewOrNull(guildId: Snowflake): GuildPreview? = catchNotFound {
        val discordPreview = guild.getGuildPreview(guildId)
        GuildPreview(GuildPreviewData.from(discordPreview), kord)
    }

    override suspend fun getMemberOrNull(guildId: Snowflake, userId: Snowflake): Member? = catchNotFound {
        val member = guild.getGuildMember(guildId = guildId, userId = userId)
        val memberData = member.toData(guildId = guildId, userId = userId)
        val userData = member.user.value!!.toData()
        Member(memberData, userData, kord)
    }

    override suspend fun getMessageOrNull(channelId: Snowflake, messageId: Snowflake): Message? = catchNotFound {
        Message(channel.getMessage(channelId = channelId, messageId = messageId).toData(), kord)
    }

    // maxBatchSize: see https://discord.com/developers/docs/resources/channel#get-channel-messages
    override fun getMessagesAfter(messageId: Snowflake, channelId: Snowflake, limit: Int?): Flow =
        limitedPagination(limit, maxBatchSize = 100) { batchSize ->
            paginateForwards(batchSize, start = messageId, idSelector = { it.id }) { after ->
                channel.getMessages(channelId, position = after, limit = batchSize)
            }
        }.map {
            val data = MessageData.from(it)
            Message(data, kord)
        }

    // maxBatchSize: see https://discord.com/developers/docs/resources/channel#get-channel-messages
    override fun getMessagesBefore(messageId: Snowflake, channelId: Snowflake, limit: Int?): Flow =
        limitedPagination(limit, maxBatchSize = 100) { batchSize ->
            paginateBackwards(batchSize, start = messageId, idSelector = { it.id }) { before ->
                channel.getMessages(channelId, position = before, limit = batchSize)
            }
        }.map {
            val data = MessageData.from(it)
            Message(data, kord)
        }

    override fun getMessagesAround(messageId: Snowflake, channelId: Snowflake, limit: Int): Flow {
        require(limit in 1..100) { "Expected limit to be in 1..100, but was $limit" }
        return flow {
            val responses = channel.getMessages(channelId, Position.Around(messageId), limit)
            for (response in responses) {
                val data = MessageData.from(response)
                emit(Message(data, kord))
            }
        }
    }

    override suspend fun getSelfOrNull(): User? = catchNotFound {
        User(user.getCurrentUser().toData(), kord)
    }

    override suspend fun getUserOrNull(id: Snowflake): User? = catchNotFound { User(user.getUser(id).toData(), kord) }

    override suspend fun getRoleOrNull(guildId: Snowflake, roleId: Snowflake): Role? = catchNotFound {
        val response = guild.getGuildRoles(guildId)
            .firstOrNull { it.id == roleId } ?: return@catchNotFound null

        Role(RoleData.from(guildId, response), kord)
    }

    override suspend fun getGuildBanOrNull(guildId: Snowflake, userId: Snowflake): Ban? = catchNotFound {
        val response = guild.getGuildBan(guildId, userId)
        val data = BanData.from(guildId, response)
        Ban(data, kord)
    }

    override fun getGuildRoles(guildId: Snowflake): Flow = flow {
        for (roleData in guild.getGuildRoles(guildId))
            emit(Role(RoleData.from(guildId, roleData), kord))
    }

    // maxBatchSize: see https://discord.com/developers/docs/resources/guild#get-guild-bans
    override fun getGuildBans(guildId: Snowflake, limit: Int?): Flow =
        limitedPagination(limit, maxBatchSize = 1000) { batchSize ->
            paginateForwards(batchSize, idSelector = { it.user.id }) { after ->
                guild.getGuildBans(guildId, position = after, limit = batchSize)
            }
        }.map {
            val data = BanData.from(guildId, it)
            Ban(data, kord)
        }

    // maxBatchSize: see https://discord.com/developers/docs/resources/guild#list-guild-members
    override fun getGuildMembers(guildId: Snowflake, limit: Int?): Flow =
        limitedPagination(limit, maxBatchSize = 1000) { batchSize ->
            paginateForwards(batchSize, idSelector = { it.user.value!!.id }) { after ->
                guild.getGuildMembers(guildId, after, limit = batchSize)
            }
        }.map {
            val userData = it.user.value!!.toData()
            val memberData = it.toData(guildId = guildId, userId = it.user.value!!.id)
            Member(memberData, userData, kord)
        }


    override fun getGuildVoiceRegions(guildId: Snowflake): Flow = flow {
        for (region in guild.getGuildVoiceRegions(guildId)) {
            val data = RegionData.from(guildId.optionalSnowflake(), region)
            emit(Region(data, kord))
        }
    }

    public fun getReactors(channelId: Snowflake, messageId: Snowflake, emoji: ReactionEmoji): Flow =
        // max batchSize/limit: see https://discord.com/developers/docs/resources/channel#get-reactions
        paginateForwards(batchSize = 100, idSelector = { it.id }) { after ->
            channel.getReactions(channelId, messageId, emoji = emoji.urlFormat, after, limit = 100)
        }.map {
            val data = UserData.from(it)
            User(data, kord)
        }

    override suspend fun getEmojiOrNull(guildId: Snowflake, emojiId: Snowflake): GuildEmoji? = catchNotFound {
        val data = EmojiData.from(guildId, emojiId, emoji.getEmoji(guildId, emojiId))
        GuildEmoji(data, kord)
    }

    override fun getEmojis(guildId: Snowflake): Flow = flow {
        for (emoji in emoji.getEmojis(guildId)) {
            val data = EmojiData.from(guildId = guildId, id = emoji.id!!, entity = emoji)
            emit(GuildEmoji(data, kord))
        }
    }

    // maxBatchSize: see https://discord.com/developers/docs/resources/user#get-current-user-guilds
    override fun getCurrentUserGuilds(limit: Int?): Flow =
        limitedPagination(limit, maxBatchSize = 200) { batchSize ->
            paginateForwards(batchSize, idSelector = { it.id }) { after ->
                user.getCurrentUserGuilds(position = after, limit = batchSize)
            }
        }.map {
            val data = guild.getGuild(it.id).toData()
            Guild(data, kord)
        }

    override fun getChannelWebhooks(channelId: Snowflake): Flow = flow {
        for (webhook in webhook.getChannelWebhooks(channelId)) {
            val data = WebhookData.from(webhook)
            emit(Webhook(data, kord))
        }
    }

    override fun getGuildWebhooks(guildId: Snowflake): Flow = flow {
        for (webhook in webhook.getGuildWebhooks(guildId)) {
            val data = WebhookData.from(webhook)
            emit(Webhook(data, kord))
        }
    }

    override suspend fun getWebhookOrNull(id: Snowflake): Webhook? = catchNotFound {
        val data = WebhookData.from(webhook.getWebhook(id))
        Webhook(data, kord)
    }

    override suspend fun getWebhookWithTokenOrNull(id: Snowflake, token: String): Webhook? = catchNotFound {
        val data = WebhookData.from(webhook.getWebhookWithToken(id, token))
        Webhook(data, kord)
    }

    override suspend fun getWebhookMessageOrNull(
        webhookId: Snowflake,
        token: String,
        messageId: Snowflake,
        threadId: Snowflake?,
    ): Message? = catchNotFound {
        val response = webhook.getWebhookMessage(webhookId, token, messageId, threadId)
        val data = MessageData.from(response)
        Message(data, kord)
    }

    public suspend fun getInviteOrNull(
        code: String,
        withCounts: Boolean = true,
        withExpiration: Boolean = true,
        scheduledEventId: Snowflake? = null,
    ): Invite? = catchNotFound {
        val response = invite.getInvite(code, withCounts, withExpiration, scheduledEventId)
        Invite(InviteData.from(response), kord)
    }

    public suspend fun getInvite(
        code: String,
        withCounts: Boolean = true,
        withExpiration: Boolean = true,
        scheduledEventId: Snowflake? = null,
    ): Invite = getInviteOrNull(code, withCounts, withExpiration, scheduledEventId)
        ?: EntityNotFoundException.inviteNotFound(code)

    /**
     * Requests to get the information of the current application.
     *
     * Entities will be fetched from Discord directly, ignoring any cached values.
     * @throws RestRequestException when the request failed.
     */
    public suspend fun getApplicationInfo(): Application {
        val response = application.getCurrentApplicationInfo()
        return Application(ApplicationData.from(response), kord)
    }

    override suspend fun getGuildWidgetOrNull(guildId: Snowflake): GuildWidget? = catchNotFound {
        val response = guild.getGuildWidget(guildId)
        GuildWidget(GuildWidgetData.from(response), guildId, kord)
    }

    override suspend fun getTemplateOrNull(code: String): Template? = catchNotFound {
        val response = template.getGuildTemplate(code)
        Template(response.toData(), kord)
    }

    override fun getTemplates(guildId: Snowflake): Flow