diff --git a/src/grom.gleam b/src/grom.gleam index 1d29e9c..3520755 100644 --- a/src/grom.gleam +++ b/src/grom.gleam @@ -1,5 +1,6 @@ import datebook/weekday.{type Weekday} import gleam/bit_array +import gleam/bool import gleam/dict.{type Dict} import gleam/dynamic/decode.{type Decoder} import gleam/float @@ -17726,3 +17727,471 @@ pub fn get_user_voice_state_response( ) -> Result(VoiceState, RestError) { handle_response(response, decode_with: voice_state_decoder()) } + +/// An "entitlement" is something a user gets access to by purchasing an +/// application's premium offer (SKU). +pub type Entitlement { + Entitlement( + id: Snowflake(Entitlement), + sku_id: Snowflake(Sku), + application_id: Snowflake(Application), + type_: EntitlementType, + is_deleted: Bool, + start_timestamp: Option(Timestamp), + end_timestamp: Option(Timestamp), + holder: EntitlementHolder, + /// Only present on one-time consumable entitlements. + is_consumed: Option(Bool), + ) +} + +fn entitlement_decoder() -> Decoder(Entitlement) { + use id <- decode.field("id", snowflake_decoder()) + use sku_id <- decode.field("sku_id", snowflake_decoder()) + use application_id <- decode.field("application_id", snowflake_decoder()) + use type_ <- decode.field("type", entitlement_type_decoder()) + use is_deleted <- decode.field("deleted", decode.bool) + use start_timestamp <- decode.field( + "starts_at", + decode.optional(rfc3339_decoder()), + ) + use end_timestamp <- decode.field( + "ends_at", + decode.optional(rfc3339_decoder()), + ) + use holder <- decode.then(entitlement_holder_decoder()) + use is_consumed <- decode.optional_field( + "consumed", + None, + decode.map(decode.bool, Some), + ) + decode.success(Entitlement( + id:, + sku_id:, + application_id:, + type_:, + is_deleted:, + start_timestamp:, + end_timestamp:, + holder:, + is_consumed:, + )) +} + +pub type EntitlementHolder { + /// The perks were given to a user. + UserEntitlementHolder(id: Snowflake(User)) + /// The perks were given to a guild. + GuildEntitlementHolder(id: Snowflake(Guild)) +} + +fn entitlement_holder_decoder() -> Decoder(EntitlementHolder) { + let user_decoder = { + use id <- decode.field("user_id", snowflake_decoder()) + decode.success(UserEntitlementHolder(id:)) + } + + let guild_decoder = { + use id <- decode.field("guild_id", snowflake_decoder()) + decode.success(GuildEntitlementHolder(id:)) + } + + decode.one_of(user_decoder, or: [guild_decoder]) +} + +pub type EntitlementType { + /// The entitlement was purchased by a user. + EntitlementThroughPurchase + /// The entitlement is for a Discord Nitro subscription. + EntitlementThroughPremiumSubscription + /// The entitlement was gifted to a user/guild by the application's developer. + EntitlementThroughDeveloperGift + /// The entitlement was purchased by a developer in application test mode. + EntitlementThroughTestModePurchase + /// The entitlement was granted when the SKU was free. + EntitlementThroughFreePurchase + /// The entitlement was gifted to a user/guild by another user. + EntitlementThroughUserGift + /// The entitlement was claimed by user for free as a Nitro subscriber. + EntitlementThroughPremiumPurchase + /// The entitlement was purchased as an application subscription. + EntitlementThroughApplicationSubscription +} + +fn entitlement_type_decoder() -> Decoder(EntitlementType) { + use variant <- decode.then(decode.int) + case variant { + 1 -> decode.success(EntitlementThroughPurchase) + 2 -> decode.success(EntitlementThroughPremiumSubscription) + 3 -> decode.success(EntitlementThroughDeveloperGift) + 4 -> decode.success(EntitlementThroughTestModePurchase) + 5 -> decode.success(EntitlementThroughFreePurchase) + 6 -> decode.success(EntitlementThroughUserGift) + 7 -> decode.success(EntitlementThroughPremiumPurchase) + 8 -> decode.success(EntitlementThroughApplicationSubscription) + _ -> decode.failure(EntitlementThroughPurchase, "EntitlementType") + } +} + +/// A "subscription" is a user making recurring payments for at least one SKU. +/// +/// Successful subscriptions grant access to the SKU's entitlements. +pub type Subscription { + Subscription( + id: Snowflake(Subscription), + user_id: Snowflake(User), + sku_ids: List(Snowflake(Sku)), + entitlement_ids: List(Snowflake(Entitlement)), + /// The SKUs that the user will be subscribed to at renewal. + renewal_sku_ids: List(Snowflake(Sku)), + current_period_start: Timestamp, + current_period_end: Timestamp, + status: SubscriptionStatus, + canceled_timestamp: Option(Timestamp), + ) +} + +fn subscription_decoder() -> Decoder(Subscription) { + use id <- decode.field("id", snowflake_decoder()) + use user_id <- decode.field("user_id", snowflake_decoder()) + use sku_ids <- decode.field("sku_ids", decode.list(snowflake_decoder())) + use entitlement_ids <- decode.field( + "entitlement_ids", + decode.list(snowflake_decoder()), + ) + use renewal_sku_ids <- decode.field("renewal_sku_ids", { + use option <- decode.then( + decode.optional(decode.list(of: snowflake_decoder())), + ) + case option { + Some(list) -> decode.success(list) + None -> decode.success([]) + } + }) + use current_period_start <- decode.field( + "current_period_start", + rfc3339_decoder(), + ) + use current_period_end <- decode.field( + "current_period_end", + rfc3339_decoder(), + ) + use status <- decode.field("status", subscription_status_decoder()) + use canceled_timestamp <- decode.field( + "canceled_at", + decode.optional(rfc3339_decoder()), + ) + decode.success(Subscription( + id:, + user_id:, + sku_ids:, + entitlement_ids:, + renewal_sku_ids:, + current_period_start:, + current_period_end:, + status:, + canceled_timestamp:, + )) +} + +pub type SubscriptionStatus { + /// The subscription is active and scheduled to renew. + SubscriptionIsActive + /// The subscription is not being charged. + SubscriptionIsInactive + /// The subscription is active but won't renew. + SubscriptionIsEnding +} + +fn subscription_status_decoder() -> Decoder(SubscriptionStatus) { + use variant <- decode.then(decode.int) + case variant { + 0 -> decode.success(SubscriptionIsActive) + 1 -> decode.success(SubscriptionIsInactive) + 2 -> decode.success(SubscriptionIsEnding) + _ -> decode.failure(SubscriptionIsActive, "SubscriptionStatus") + } +} + +pub opaque type GetCurrentApplicationEntitlementsOptions { + GetCurrentApplicationEntitlementsOptions( + user_id: Option(Snowflake(User)), + sku_ids: Option(List(Snowflake(Sku))), + before: Option(Snowflake(Entitlement)), + after: Option(Snowflake(Entitlement)), + limit: Option(Int), + guild_id: Option(Snowflake(Guild)), + exclude_ended: Option(Bool), + exclude_deleted: Option(Bool), + ) +} + +fn get_current_application_entitlement_options_to_query( + options: GetCurrentApplicationEntitlementsOptions, +) -> List(#(String, String)) { + [ + optional_to_string(options.user_id, "user_id", snowflake_to_string), + optional_to_string(options.sku_ids, "sku_ids", fn(ids) { + ids + |> list.map(snowflake_to_string) + |> string.join(",") + }), + optional_to_string(options.before, "before", snowflake_to_string), + optional_to_string(options.after, "after", snowflake_to_string), + optional_to_string(options.limit, "limit", int.to_string), + optional_to_string(options.guild_id, "guild_id", snowflake_to_string), + optional_to_string(options.exclude_ended, "exclude_ended", bool.to_string), + optional_to_string( + options.exclude_deleted, + "exclude_deleted", + bool.to_string, + ), + ] + |> list.filter_map(function.identity) +} + +/// Defaults: +/// - limit 100 +/// - include ended +/// - exclude deleted +pub fn new_get_current_application_entitlements_options() -> GetCurrentApplicationEntitlementsOptions { + GetCurrentApplicationEntitlementsOptions( + user_id: None, + sku_ids: None, + before: None, + after: None, + limit: None, + guild_id: None, + exclude_ended: None, + exclude_deleted: None, + ) +} + +pub fn get_current_application_entitlements_for_user( + options: GetCurrentApplicationEntitlementsOptions, + user_with_id user_id: Snowflake(User), +) -> GetCurrentApplicationEntitlementsOptions { + GetCurrentApplicationEntitlementsOptions(..options, user_id: Some(user_id)) +} + +pub fn get_current_application_entitlements_before( + options: GetCurrentApplicationEntitlementsOptions, + entitlement_with_id before: Snowflake(Entitlement), +) -> GetCurrentApplicationEntitlementsOptions { + GetCurrentApplicationEntitlementsOptions(..options, before: Some(before)) +} + +pub fn get_current_application_entitlements_after( + options: GetCurrentApplicationEntitlementsOptions, + entitlement_with_id after: Snowflake(Entitlement), +) -> GetCurrentApplicationEntitlementsOptions { + GetCurrentApplicationEntitlementsOptions(..options, after: Some(after)) +} + +pub fn get_current_application_entitlements_for_skus( + options: GetCurrentApplicationEntitlementsOptions, + skus_with_ids sku_ids: List(Snowflake(Sku)), +) -> GetCurrentApplicationEntitlementsOptions { + GetCurrentApplicationEntitlementsOptions(..options, sku_ids: Some(sku_ids)) +} + +/// Limit must be between 1 and 100. +pub fn get_current_application_entitlements_with_limit( + options: GetCurrentApplicationEntitlementsOptions, + limit: Int, +) -> GetCurrentApplicationEntitlementsOptions { + GetCurrentApplicationEntitlementsOptions(..options, limit: Some(limit)) +} + +pub fn get_current_application_entitlements_for_guild( + options: GetCurrentApplicationEntitlementsOptions, + guild_with_id guild_id: Snowflake(Guild), +) -> GetCurrentApplicationEntitlementsOptions { + GetCurrentApplicationEntitlementsOptions(..options, guild_id: Some(guild_id)) +} + +pub fn get_current_application_entitlements_excluding_ended( + options: GetCurrentApplicationEntitlementsOptions, +) -> GetCurrentApplicationEntitlementsOptions { + GetCurrentApplicationEntitlementsOptions(..options, exclude_ended: Some(True)) +} + +pub fn get_current_application_entitlements_including_deleted( + options: GetCurrentApplicationEntitlementsOptions, +) -> GetCurrentApplicationEntitlementsOptions { + GetCurrentApplicationEntitlementsOptions( + ..options, + exclude_deleted: Some(False), + ) +} + +/// You have to provide the current application's ID. Sorry, I don't make the rules :/ +pub fn get_current_application_entitlements_request( + token token: Token, + current_application_id application_id: Snowflake(Application), + using options: GetCurrentApplicationEntitlementsOptions, +) -> Request(String) { + let query = options |> get_current_application_entitlement_options_to_query + + new_request( + token:, + to: "/applications/" + <> snowflake_to_string(application_id) + <> "/entitlements", + method: http.Get, + ) + |> request.set_query(query) +} + +pub fn get_current_application_entitlements_response( + response: Response(String), +) -> Result(List(Entitlement), RestError) { + handle_response(response, decode_with: decode.list(of: entitlement_decoder())) +} + +/// You have to provide the current application's ID. Sorry, I don't make the rules :/ +pub fn get_entitlement_request( + token token: Token, + current_application_id application_id: Snowflake(Application), + entitlement_with_id entitlement_id: Snowflake(Entitlement), +) -> Request(String) { + new_request( + token:, + to: "/applications/" + <> snowflake_to_string(application_id) + <> "/entitlements/" + <> snowflake_to_string(entitlement_id), + method: http.Get, + ) +} + +pub fn get_entitlement_response( + response: Response(String), +) -> Result(Entitlement, RestError) { + handle_response(response, decode_with: entitlement_decoder()) +} + +/// Marks the entitlement as consumed by the user. +/// +/// Only for one-time consumable SKUs. +/// +/// You have to provide the current application's ID. Sorry, I don't make the rules :/ +pub fn consume_entitlement_request( + token token: Token, + current_application_id application_id: Snowflake(Application), + entitlement_with_id entitlement_id: Snowflake(Entitlement), +) -> Request(String) { + new_request( + token:, + to: "/applications/" + <> snowflake_to_string(application_id) + <> "/entitlements/" + <> snowflake_to_string(entitlement_id) + <> "/consume", + method: http.Post, + ) +} + +pub fn consume_entitlement_response( + response: Response(String), +) -> Result(Nil, RestError) { + handle_no_content_response(response) +} + +// skipped create/delete test entitlement, couldnt bother implementing the partial object +// if anyone needs it, just add it + +pub opaque type GetSkuSubscriptionsOptions { + GetSkuSubscriptionsOptions( + before: Option(Snowflake(Subscription)), + after: Option(Snowflake(Subscription)), + limit: Option(Int), + user_id: Snowflake(User), + ) +} + +/// Default limit is 50. +pub fn new_get_sku_subscriptions_options( + for_user_with_id user_id: Snowflake(User), +) -> GetSkuSubscriptionsOptions { + GetSkuSubscriptionsOptions(before: None, after: None, limit: None, user_id:) +} + +pub fn get_sku_subscriptions_before( + options: GetSkuSubscriptionsOptions, + subscription_with_id before: Snowflake(Subscription), +) -> GetSkuSubscriptionsOptions { + GetSkuSubscriptionsOptions(..options, before: Some(before)) +} + +pub fn get_sku_subscriptions_after( + options: GetSkuSubscriptionsOptions, + subscription_with_id after: Snowflake(Subscription), +) -> GetSkuSubscriptionsOptions { + GetSkuSubscriptionsOptions(..options, after: Some(after)) +} + +/// Limit must be between 1 and 100. +pub fn get_sku_subscriptions_with_limit( + options: GetSkuSubscriptionsOptions, + limit: Int, +) -> GetSkuSubscriptionsOptions { + GetSkuSubscriptionsOptions(..options, limit: Some(limit)) +} + +fn get_sku_subscriptions_options_to_query( + options: GetSkuSubscriptionsOptions, +) -> List(#(String, String)) { + [ + optional_to_string(options.before, "before", snowflake_to_string), + optional_to_string(options.after, "after", snowflake_to_string), + optional_to_string(options.limit, "limit", int.to_string), + Ok(#("user_id", snowflake_to_string(options.user_id))), + ] + |> list.filter_map(function.identity) +} + +pub fn get_sku_subscriptions_request( + token token: Token, + for_sku_with_id sku_id: Snowflake(Sku), + using options: GetSkuSubscriptionsOptions, +) -> Request(String) { + let query = options |> get_sku_subscriptions_options_to_query + + new_request( + token:, + to: "/skus/" <> snowflake_to_string(sku_id) <> "/subscriptions", + method: http.Get, + ) + |> request.set_query(query) +} + +pub fn get_sku_subscriptions_response( + response: Response(String), +) -> Result(List(Subscription), RestError) { + handle_response( + response, + decode_with: decode.list(of: subscription_decoder()), + ) +} + +pub fn get_sku_subscription_request( + token token: Token, + for_sku_with_id sku_id: Snowflake(Sku), + subscription_with_id subscription_id: Snowflake(Subscription), +) -> Request(String) { + new_request( + token:, + to: "/skus/" + <> snowflake_to_string(sku_id) + <> "/subscriptions/" + <> snowflake_to_string(subscription_id), + method: http.Get, + ) +} + +pub fn get_sku_subscription_response( + response: Response(String), +) -> Result(Subscription, RestError) { + handle_response(response, decode_with: subscription_decoder()) +}