Skip to content

Commit

Permalink
feat(core): subscriptions
Browse files Browse the repository at this point in the history
  • Loading branch information
lukellmann committed Sep 12, 2024
1 parent 6d74f96 commit c87d748
Show file tree
Hide file tree
Showing 20 changed files with 794 additions and 74 deletions.
158 changes: 152 additions & 6 deletions core/api/core.api

Large diffs are not rendered by default.

172 changes: 167 additions & 5 deletions core/api/core.klib.api

Large diffs are not rendered by default.

58 changes: 2 additions & 56 deletions core/src/commonMain/kotlin/Kord.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,12 @@ import dev.kord.cache.api.DataCache
import dev.kord.common.annotation.KordExperimental
import dev.kord.common.annotation.KordUnsafe
import dev.kord.common.entity.DiscordShard
import dev.kord.common.entity.EntitlementOwnerType
import dev.kord.common.entity.Snowflake
import dev.kord.common.exception.RequestException
import dev.kord.core.behavior.GuildBehavior
import dev.kord.core.behavior.UserBehavior
import dev.kord.core.builder.kord.KordBuilder
import dev.kord.core.builder.kord.KordProxyBuilder
import dev.kord.core.builder.kord.KordRestOnlyBuilder
import dev.kord.core.cache.data.ApplicationCommandData
import dev.kord.core.cache.data.EntitlementData
import dev.kord.core.cache.data.GuildData
import dev.kord.core.cache.data.UserData
import dev.kord.core.entity.*
Expand All @@ -36,7 +32,6 @@ import dev.kord.rest.builder.guild.GuildCreateBuilder
import dev.kord.rest.builder.interaction.*
import dev.kord.rest.builder.monetization.EntitlementsListRequestBuilder
import dev.kord.rest.builder.user.CurrentUserModifyBuilder
import dev.kord.rest.json.request.TestEntitlementCreateRequest
import dev.kord.rest.request.RestRequestException
import dev.kord.rest.service.RestClient
import io.github.oshai.kotlinlogging.KotlinLogging
Expand Down Expand Up @@ -378,8 +373,7 @@ public class Kord(
*
* @throws RestRequestException if something went wrong during the request.
*/
public suspend fun getSkus(): List<Sku> =
rest.sku.listSkus(selfId).map { Sku(it, this) }
public suspend fun getSkus(): List<Sku> = rest.sku.listSkus(selfId).map { Sku(it, this) }

/**
* Requests to get all [Entitlement]s for this application.
Expand All @@ -391,58 +385,10 @@ public class Kord(
builder: EntitlementsListRequestBuilder.() -> Unit = {},
): Flow<Entitlement> {
contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) }

val request = EntitlementsListRequestBuilder()
.apply(builder)
.toRequest()

val request = EntitlementsListRequestBuilder().apply(builder).toRequest()
return strategy.supply(this).getEntitlements(selfId, request)
}

/**
* Requests to create a new [test entitlement][Entitlement] to a [Sku] with the given [skuId] for an owner with the
* given [ownerId] and [ownerType]. Discord will act as though that owner has entitlement to your premium offering.
*
* The returned [Entitlement] will not contain [startsAt][Entitlement.startsAt] and [endsAt][Entitlement.endsAt], as
* it's valid in perpetuity.
*
* @throws RestRequestException if something went wrong during the request.
*/
public suspend fun createTestEntitlement(
skuId: Snowflake,
ownerId: Snowflake,
ownerType: EntitlementOwnerType,
): Entitlement {
val response =
rest.entitlement.createTestEntitlement(selfId, TestEntitlementCreateRequest(skuId, ownerId, ownerType))
val data = EntitlementData.from(response)

return Entitlement(data, this)
}

/**
* Requests to create a new [test entitlement][Entitlement] to a [Sku] with the given [skuId] for a given [user].
* Discord will act as though that user has entitlement to your premium offering.
*
* The returned [Entitlement] will not contain [startsAt][Entitlement.startsAt] and [endsAt][Entitlement.endsAt], as
* it's valid in perpetuity.
*
* @throws RestRequestException if something went wrong during the request.
*/
public suspend fun createTestEntitlement(skuId: Snowflake, user: UserBehavior): Entitlement =
createTestEntitlement(skuId, user.id, EntitlementOwnerType.User)

/**
* Requests to create a new [test entitlement][Entitlement] to a [Sku] with the given [skuId] for a given [guild].
* Discord will act as though that guild has entitlement to your premium offering.
*
* The returned [Entitlement] will not contain [startsAt][Entitlement.startsAt] and [endsAt][Entitlement.endsAt], as
* it's valid in perpetuity.
*
* @throws RestRequestException if something went wrong during the request.
*/
public suspend fun createTestEntitlement(skuId: Snowflake, guild: GuildBehavior): Entitlement =
createTestEntitlement(skuId, guild.id, EntitlementOwnerType.Guild)

public suspend fun getSticker(id: Snowflake): Sticker = defaultSupplier.getSticker(id)

Expand Down
5 changes: 5 additions & 0 deletions core/src/commonMain/kotlin/Unsafe.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import dev.kord.core.behavior.channel.threads.ThreadChannelBehavior
import dev.kord.core.behavior.channel.threads.ThreadParentChannelBehavior
import dev.kord.core.behavior.interaction.ApplicationCommandInteractionBehavior
import dev.kord.core.behavior.interaction.ComponentInteractionBehavior
import dev.kord.core.behavior.monetization.SkuBehavior
import dev.kord.core.behavior.monetization.SkuBehaviorImpl
import dev.kord.rest.service.InteractionService

/**
Expand Down Expand Up @@ -46,6 +48,9 @@ public class Unsafe(private val kord: Kord) {
ruleId: Snowflake,
): MentionSpamAutoModerationRuleBehavior = MentionSpamAutoModerationRuleBehaviorImpl(guildId, ruleId, kord)

public fun sku(skuId: Snowflake, applicationId: Snowflake = kord.selfId): SkuBehavior =
SkuBehaviorImpl(applicationId, skuId, kord)

public fun message(channelId: Snowflake, messageId: Snowflake): MessageBehavior =
MessageBehavior(channelId = channelId, messageId = messageId, kord = kord)

Expand Down
122 changes: 122 additions & 0 deletions core/src/commonMain/kotlin/behavior/monetization/SkuBehavior.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package dev.kord.core.behavior.monetization

import dev.kord.common.entity.EntitlementOwnerType
import dev.kord.common.entity.Snowflake
import dev.kord.common.exception.RequestException
import dev.kord.core.Kord
import dev.kord.core.behavior.GuildBehavior
import dev.kord.core.behavior.UserBehavior
import dev.kord.core.cache.data.EntitlementData
import dev.kord.core.entity.Application
import dev.kord.core.entity.KordEntity
import dev.kord.core.entity.Strategizable
import dev.kord.core.entity.monetization.Entitlement
import dev.kord.core.entity.monetization.Sku
import dev.kord.core.entity.monetization.Subscription
import dev.kord.core.exception.EntityNotFoundException
import dev.kord.core.hash
import dev.kord.core.supplier.EntitySupplier
import dev.kord.core.supplier.EntitySupplyStrategy
import dev.kord.rest.builder.monetization.SkuSubscriptionsListRequestBuilder
import dev.kord.rest.json.request.TestEntitlementCreateRequest
import dev.kord.rest.request.RestRequestException
import kotlinx.coroutines.flow.Flow
import kotlin.contracts.InvocationKind.EXACTLY_ONCE
import kotlin.contracts.contract

/** The behavior of an [Sku]. */
public interface SkuBehavior : KordEntity, Strategizable {

/** The ID of the [Application] this SKU is for. */
public val applicationId: Snowflake

/**
* Requests a [Subscription] containing this SKU by its [id][subscriptionId]. Returns `null` if it wasn't found.
*
* @throws RequestException if something went wrong during the request.
*/
public suspend fun getSubscriptionOrNull(subscriptionId: Snowflake): Subscription? =
supplier.getSubscriptionOrNull(this.id, subscriptionId)

/**
* Requests a [Subscription] containing this SKU by its [id][subscriptionId].
*
* @throws RequestException if something went wrong during the request.
* @throws EntityNotFoundException if the [Subscription] wasn't found.
*/
public suspend fun getSubscription(subscriptionId: Snowflake): Subscription =
supplier.getSubscription(this.id, subscriptionId)

/**
* Requests to create a new [test entitlement][Entitlement] to this SKU for an owner with the given [ownerId] and
* [ownerType]. Discord will act as though that owner has entitlement to your premium offering.
*
* The returned [Entitlement] will not contain [startsAt][Entitlement.startsAt] and [endsAt][Entitlement.endsAt], as
* it's valid in perpetuity.
*
* @throws RestRequestException if something went wrong during the request.
*/
public suspend fun createTestEntitlement(ownerId: Snowflake, ownerType: EntitlementOwnerType): Entitlement {
val response = kord.rest.entitlement.createTestEntitlement(
applicationId,
TestEntitlementCreateRequest(this.id, ownerId, ownerType),
)
return Entitlement(EntitlementData.from(response), kord)
}

/**
* Requests to create a new [test entitlement][Entitlement] to this SKU for a given [user]. Discord will act as
* though that user has entitlement to your premium offering.
*
* The returned [Entitlement] will not contain [startsAt][Entitlement.startsAt] and [endsAt][Entitlement.endsAt], as
* it's valid in perpetuity.
*
* @throws RestRequestException if something went wrong during the request.
*/
public suspend fun createTestEntitlement(skuId: Snowflake, user: UserBehavior): Entitlement =
createTestEntitlement(user.id, EntitlementOwnerType.User)

/**
* Requests to create a new [test entitlement][Entitlement] to this SKU for a given [guild]. Discord will act as
* though that guild has entitlement to your premium offering.
*
* The returned [Entitlement] will not contain [startsAt][Entitlement.startsAt] and [endsAt][Entitlement.endsAt], as
* it's valid in perpetuity.
*
* @throws RestRequestException if something went wrong during the request.
*/
public suspend fun createTestEntitlement(skuId: Snowflake, guild: GuildBehavior): Entitlement =
createTestEntitlement(guild.id, EntitlementOwnerType.Guild)

override fun withStrategy(strategy: EntitySupplyStrategy<*>): SkuBehavior
}

/**
* Requests to get all [Subscription]s containing this [Sku].
*
* The returned flow is lazily executed, any [RequestException] will be thrown on
* [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
*/
public inline fun SkuBehavior.getSubscriptions(
builder: SkuSubscriptionsListRequestBuilder.() -> Unit,
): Flow<Subscription> {
contract { callsInPlace(builder, EXACTLY_ONCE) }
val request = SkuSubscriptionsListRequestBuilder().apply(builder).toRequest()
return supplier.getSubscriptions(this.id, request)
}

internal class SkuBehaviorImpl(
override val applicationId: Snowflake,
override val id: Snowflake,
override val kord: Kord,
override val supplier: EntitySupplier = kord.defaultSupplier,
) : SkuBehavior {
override fun withStrategy(strategy: EntitySupplyStrategy<*>) =
SkuBehaviorImpl(applicationId, id, kord, strategy.supply(kord))

override fun equals(other: Any?) =
other is SkuBehavior && this.id == other.id && this.applicationId == other.applicationId

override fun hashCode() = hash(id, applicationId)
override fun toString() = "SkuBehavior(applicationId=$applicationId, id=$id, kord=$kord, supplier=$supplier)"
}
2 changes: 2 additions & 0 deletions core/src/commonMain/kotlin/cache/DataCacheExtensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public suspend fun DataCache.registerKordData(): Unit = register(
StickerData.description,
AutoModerationRuleData.description,
EntitlementData.description,
SubscriptionData.description,
)

/**
Expand All @@ -52,6 +53,7 @@ internal suspend fun DataCache.removeKordData() {
query<StickerData>().remove()
query<AutoModerationRuleData>().remove()
query<EntitlementData>().remove()
query<SubscriptionData>().remove()
}

/**
Expand Down
4 changes: 4 additions & 0 deletions core/src/commonMain/kotlin/cache/KordCache.kt
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ public class KordCacheBuilder {
public fun entitlements(generator: Generator<EntitlementData, Snowflake>): Unit =
forDescription(EntitlementData.description, generator)

/** Configures the caching for [SubscriptionData]. */
public fun subscriptions(generator: Generator<SubscriptionData, Snowflake>): Unit =
forDescription(SubscriptionData.description, generator)

public fun build(): DataCache = DelegatingDataCache(EntrySupplier.invoke { cache, description ->
val generator = descriptionGenerators[description] ?: defaultGenerator
generator(cache, description)
Expand Down
41 changes: 41 additions & 0 deletions core/src/commonMain/kotlin/cache/data/SubscriptionData.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package dev.kord.core.cache.data

import dev.kord.cache.api.data.DataDescription
import dev.kord.cache.api.data.description
import dev.kord.common.entity.DiscordSubscription
import dev.kord.common.entity.Snowflake
import dev.kord.common.entity.SubscriptionStatus
import dev.kord.common.entity.optional.Optional
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable

@Serializable
public data class SubscriptionData(
val id: Snowflake,
val userId: Snowflake,
val skuIds: List<Snowflake>,
val entitlementIds: List<Snowflake>,
val currentPeriodStart: Instant,
val currentPeriodEnd: Instant,
val status: SubscriptionStatus,
val canceledAt: Instant?,
val country: Optional<String> = Optional.Missing(),
) {
public companion object {
public val description: DataDescription<SubscriptionData, Snowflake> = description(SubscriptionData::id)

public fun from(subscription: DiscordSubscription): SubscriptionData = with(subscription) {
SubscriptionData(
id = id,
userId = userId,
skuIds = skuIds,
entitlementIds = entitlementIds,
currentPeriodStart = currentPeriodStart,
currentPeriodEnd = currentPeriodEnd,
status = status,
canceledAt = canceledAt,
country = country,
)
}
}
}
1 change: 1 addition & 0 deletions core/src/commonMain/kotlin/cache/data/UserData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public data class UserData(
link(UserData::id to VoiceStateData::userId)
link(UserData::id to PresenceData::userId)
link(UserData::id to EntitlementData::nullableUserId)
link(UserData::id to SubscriptionData::userId)
}

public fun from(entity: DiscordUser): UserData = with(entity) {
Expand Down
16 changes: 9 additions & 7 deletions core/src/commonMain/kotlin/entity/monetization/Sku.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import dev.kord.common.entity.SkuFlags
import dev.kord.common.entity.SkuType
import dev.kord.common.entity.Snowflake
import dev.kord.core.Kord
import dev.kord.core.behavior.monetization.SkuBehavior
import dev.kord.core.entity.Application
import dev.kord.core.entity.Guild
import dev.kord.core.entity.KordEntity
import dev.kord.core.entity.User
import dev.kord.core.hash
import dev.kord.core.supplier.EntitySupplier
import dev.kord.core.supplier.EntitySupplyStrategy

/**
* An instance of an [SKU](https://discord.com/developers/docs/resources/sku).
Expand All @@ -20,7 +22,8 @@ import dev.kord.core.hash
public class Sku(
public val data: DiscordSku,
override val kord: Kord,
) : KordEntity {
override val supplier: EntitySupplier = kord.defaultSupplier,
) : SkuBehavior {
override val id: Snowflake
get() = data.id

Expand All @@ -29,10 +32,7 @@ public class Sku(
*/
public val type: SkuType get() = data.type

/**
* The ID of the [Application] this SKU is for.
*/
public val applicationId: Snowflake get() = data.applicationId
override val applicationId: Snowflake get() = data.applicationId

/**
* The customer-facing name of this premium offering.
Expand All @@ -49,8 +49,10 @@ public class Sku(
*/
public val flags: SkuFlags get() = data.flags

override fun withStrategy(strategy: EntitySupplyStrategy<*>): Sku = Sku(data, kord, strategy.supply(kord))

override fun equals(other: Any?): Boolean =
other is Sku && this.id == other.id && this.applicationId == other.applicationId
other is SkuBehavior && this.id == other.id && this.applicationId == other.applicationId

override fun hashCode(): Int = hash(id, applicationId)

Expand Down
Loading

0 comments on commit c87d748

Please sign in to comment.