diff --git a/packages/bot/src/bot.ts b/packages/bot/src/bot.ts index 8b11a40f0..31cb2749a 100644 --- a/packages/bot/src/bot.ts +++ b/packages/bot/src/bot.ts @@ -32,6 +32,7 @@ import type { Role, ScheduledEvent, Sticker, + Subscription, ThreadMember, User, VoiceState, @@ -282,6 +283,9 @@ export interface EventHandlers { entitlementCreate: (entitlement: Entitlement) => unknown entitlementUpdate: (entitlement: Entitlement) => unknown entitlementDelete: (entitlement: Entitlement) => unknown + subscriptionCreate: (subscription: Subscription) => unknown + subscriptionUpdate: (subscription: Subscription) => unknown + subscriptionDelete: (subscription: Subscription) => unknown messagePollVoteAdd: (payload: { userId: bigint; channelId: bigint; messageId: bigint; guildId?: bigint; answerId: number }) => unknown messagePollVoteRemove: (payload: { userId: bigint; channelId: bigint; messageId: bigint; guildId?: bigint; answerId: number }) => unknown } diff --git a/packages/bot/src/handlers.ts b/packages/bot/src/handlers.ts index 84d143f68..057cbfa83 100644 --- a/packages/bot/src/handlers.ts +++ b/packages/bot/src/handlers.ts @@ -72,6 +72,9 @@ export function createBotGatewayHandlers( ENTITLEMENT_DELETE: options.ENTITLEMENT_DELETE ?? handlers.handleEntitlementDelete, MESSAGE_POLL_VOTE_ADD: options.MESSAGE_POLL_VOTE_ADD ?? handlers.handleMessagePollVoteAdd, MESSAGE_POLL_VOTE_REMOVE: options.MESSAGE_POLL_VOTE_REMOVE ?? handlers.handleMessagePollVoteRemove, + SUBSCRIPTION_CREATE: options.SUBSCRIPTION_CREATE ?? handlers.handleSubscriptionCreate, + SUBSCRIPTION_UPDATE: options.SUBSCRIPTION_UPDATE ?? handlers.handleSubscriptionUpdate, + SUBSCRIPTION_DELETE: options.SUBSCRIPTION_DELETE ?? handlers.handleSubscriptionDelete, } } diff --git a/packages/bot/src/handlers/index.ts b/packages/bot/src/handlers/index.ts index bfaa705b5..9f95d83df 100644 --- a/packages/bot/src/handlers/index.ts +++ b/packages/bot/src/handlers/index.ts @@ -12,3 +12,4 @@ export * from './poll/index.js' export * from './roles/index.js' export * from './voice/index.js' export * from './webhooks/index.js' +export * from './subscriptions/index.js' diff --git a/packages/bot/src/handlers/subscriptions/SUBSCRIPTION_CREATE.ts b/packages/bot/src/handlers/subscriptions/SUBSCRIPTION_CREATE.ts new file mode 100644 index 000000000..ac9b7fbf2 --- /dev/null +++ b/packages/bot/src/handlers/subscriptions/SUBSCRIPTION_CREATE.ts @@ -0,0 +1,9 @@ +import type { DiscordGatewayPayload, DiscordSubscription } from '@discordeno/types' +import type { Bot } from '../../bot.js' + +export async function handleSubscriptionCreate(bot: Bot, data: DiscordGatewayPayload): Promise { + if (!bot.events.subscriptionCreate) return + + const payload = data.d as DiscordSubscription + bot.events.subscriptionCreate(bot.transformers.subscription(bot, payload)) +} diff --git a/packages/bot/src/handlers/subscriptions/SUBSCRIPTION_DELETE.ts b/packages/bot/src/handlers/subscriptions/SUBSCRIPTION_DELETE.ts new file mode 100644 index 000000000..30c0356e2 --- /dev/null +++ b/packages/bot/src/handlers/subscriptions/SUBSCRIPTION_DELETE.ts @@ -0,0 +1,9 @@ +import type { DiscordGatewayPayload, DiscordSubscription } from '@discordeno/types' +import type { Bot } from '../../bot.js' + +export async function handleSubscriptionDelete(bot: Bot, data: DiscordGatewayPayload): Promise { + if (!bot.events.subscriptionDelete) return + + const payload = data.d as DiscordSubscription + bot.events.subscriptionDelete(bot.transformers.subscription(bot, payload)) +} diff --git a/packages/bot/src/handlers/subscriptions/SUBSCRIPTION_UPDATE.ts b/packages/bot/src/handlers/subscriptions/SUBSCRIPTION_UPDATE.ts new file mode 100644 index 000000000..bdeb42b95 --- /dev/null +++ b/packages/bot/src/handlers/subscriptions/SUBSCRIPTION_UPDATE.ts @@ -0,0 +1,9 @@ +import type { DiscordGatewayPayload, DiscordSubscription } from '@discordeno/types' +import type { Bot } from '../../bot.js' + +export async function handleSubscriptionUpdate(bot: Bot, data: DiscordGatewayPayload): Promise { + if (!bot.events.subscriptionUpdate) return + + const payload = data.d as DiscordSubscription + bot.events.subscriptionUpdate(bot.transformers.subscription(bot, payload)) +} diff --git a/packages/bot/src/handlers/subscriptions/index.ts b/packages/bot/src/handlers/subscriptions/index.ts new file mode 100644 index 000000000..6b102b5ee --- /dev/null +++ b/packages/bot/src/handlers/subscriptions/index.ts @@ -0,0 +1,3 @@ +export * from './SUBSCRIPTION_CREATE.js' +export * from './SUBSCRIPTION_DELETE.js' +export * from './SUBSCRIPTION_UPDATE.js' diff --git a/packages/bot/src/helpers.ts b/packages/bot/src/helpers.ts index ac7565d17..c783c16f9 100644 --- a/packages/bot/src/helpers.ts +++ b/packages/bot/src/helpers.ts @@ -50,6 +50,7 @@ import type { DiscordActivityInstance, DiscordEntitlement, DiscordMessage, + DiscordSubscription, EditApplication, EditAutoModerationRuleOptions, EditBotMemberOptions, @@ -80,6 +81,7 @@ import type { InteractionResponse, ListArchivedThreads, ListGuildMembers, + ListSkuSubscriptionsOptions, MfaLevels, ModifyApplicationEmoji, ModifyChannel, @@ -762,6 +764,12 @@ export function createBotHelpers(bot: Bot): BotHelpers { listSkus: async (applicationId) => { return (await bot.rest.listSkus(applicationId)).map((sku) => bot.transformers.sku(bot, snakelize(sku))) }, + getSubscription: async (skuId, subscriptionId) => { + return await bot.rest.getSubscription(skuId, subscriptionId) + }, + listSubscriptions: async (skuId, options) => { + return await bot.rest.listSubscriptions(skuId, options) + }, } } @@ -1005,4 +1013,6 @@ export interface BotHelpers { createTestEntitlement: (applicationId: BigString, body: CreateEntitlement) => Promise> deleteTestEntitlement: (applicationId: BigString, entitlementId: BigString) => Promise listSkus: (applicationId: BigString) => Promise + listSubscriptions: (skuId: BigString, options?: ListSkuSubscriptionsOptions) => Promise> + getSubscription: (skuId: BigString, subscriptionId: BigString) => Promise> } diff --git a/packages/bot/src/transformers.ts b/packages/bot/src/transformers.ts index 2511e0951..361241ca1 100644 --- a/packages/bot/src/transformers.ts +++ b/packages/bot/src/transformers.ts @@ -54,6 +54,7 @@ import type { DiscordStageInstance, DiscordSticker, DiscordStickerPack, + DiscordSubscription, DiscordTeam, DiscordTemplate, DiscordThreadMember, @@ -118,6 +119,7 @@ import { type StageInstance, type Sticker, type StickerPack, + type Subscription, type Team, type Template, type ThreadMember, @@ -204,6 +206,7 @@ import { transformCreateApplicationCommandToDiscordCreateApplicationCommand, transformInteractionResponseToDiscordInteractionResponse, } from './transformers/reverse/index.js' +import { transformSubscription } from './transformers/subscription.js' import type { BotInteractionResponse, DiscordComponent, @@ -280,6 +283,7 @@ export interface Transformers { stageInstance: (bot: Bot, payload: DiscordStageInstance, stageInstance: StageInstance) => any sticker: (bot: Bot, payload: DiscordSticker, sticker: Sticker) => any stickerPack: (bot: Bot, payload: DiscordStickerPack, stickerPack: StickerPack) => any + subscription: (bot: Bot, payload: DiscordSubscription, subscription: Subscription) => any team: (bot: Bot, payload: DiscordTeam, team: Team) => any template: (bot: Bot, payload: DiscordTemplate, template: Template) => any threadMember: (bot: Bot, payload: DiscordThreadMember, threadMember: ThreadMember) => any @@ -360,6 +364,7 @@ export interface Transformers { stageInstance: (bot: Bot, payload: DiscordStageInstance) => StageInstance sticker: (bot: Bot, payload: DiscordSticker) => Sticker stickerPack: (bot: Bot, payload: DiscordStickerPack) => StickerPack + subscription: (bot: Bot, payload: DiscordSubscription) => Subscription team: (bot: Bot, payload: DiscordTeam) => Team template: (bot: Bot, payload: DiscordTemplate) => Template threadMember: (bot: Bot, payload: DiscordThreadMember) => ThreadMember @@ -722,6 +727,17 @@ export interface TransformersDesiredProperties { user: boolean sortValue: boolean } + subscription: { + id: boolean + userId: boolean + skuIds: boolean + entitlementIds: boolean + currentPeriodStart: boolean + currentPeriodEnd: boolean + status: boolean + canceledAt: boolean + country: boolean + } user: { username: boolean globalName: boolean @@ -905,6 +921,7 @@ export function createTransformers(options: RecursivePartial, opts stageInstance: options.customizers?.stageInstance ?? defaultCustomizer, sticker: options.customizers?.sticker ?? defaultCustomizer, stickerPack: options.customizers?.stickerPack ?? defaultCustomizer, + subscription: options.customizers?.subscription ?? defaultCustomizer, team: options.customizers?.team ?? defaultCustomizer, template: options.customizers?.template ?? defaultCustomizer, threadMember: options.customizers?.threadMember ?? defaultCustomizer, @@ -986,6 +1003,7 @@ export function createTransformers(options: RecursivePartial, opts stageInstance: options.stageInstance ?? transformStageInstance, sticker: options.sticker ?? transformSticker, stickerPack: options.stickerPack ?? transformStickerPack, + subscription: options.subscription ?? transformSubscription, team: options.team ?? transformTeam, template: options.template ?? transformTemplate, threadMember: options.threadMember ?? transformThreadMember, @@ -1379,6 +1397,18 @@ export function createDesiredPropertiesObject( sortValue: defaultValue, ...desiredProperties.sticker, }, + subscription: { + id: defaultValue, + userId: defaultValue, + skuIds: defaultValue, + entitlementIds: defaultValue, + currentPeriodStart: defaultValue, + currentPeriodEnd: defaultValue, + status: defaultValue, + canceledAt: defaultValue, + country: defaultValue, + ...desiredProperties.subscription, + }, user: { username: defaultValue, globalName: defaultValue, diff --git a/packages/bot/src/transformers/subscription.ts b/packages/bot/src/transformers/subscription.ts new file mode 100644 index 000000000..aaee9ae7f --- /dev/null +++ b/packages/bot/src/transformers/subscription.ts @@ -0,0 +1,19 @@ +import type { DiscordSubscription } from '@discordeno/types' +import type { Bot, Subscription } from '../index.js' + +export function transformSubscription(bot: Bot, payload: DiscordSubscription): Subscription { + const props = bot.transformers.desiredProperties.subscription + const subscription = {} as Subscription + + if (props.id && payload.id) subscription.id = bot.transformers.snowflake(payload.id) + if (props.userId && payload.user_id) subscription.userId = bot.transformers.snowflake(payload.user_id) + if (props.skuIds && payload.sku_ids) subscription.skuIds = payload.sku_ids.map((skuId) => bot.transformers.snowflake(skuId)) + if (props.entitlementIds && payload.entitlement_ids) subscription.entitlementIds = payload.entitlement_ids.map((entitlementId) => bot.transformers.snowflake(entitlementId)) + if (props.currentPeriodStart && payload.current_period_start) subscription.currentPeriodStart = Date.parse(payload.current_period_start) + if (props.currentPeriodEnd && payload.current_period_end) subscription.currentPeriodEnd = Date.parse(payload.current_period_end) + if (props.status && payload.status) subscription.status = payload.status + if (props.canceledAt && payload.canceled_at) subscription.canceledAt = Date.parse(payload.canceled_at) + if (props.country && payload.country) subscription.country = payload.country + + return bot.transformers.customizers.subscription(bot, payload, subscription) +} diff --git a/packages/bot/src/transformers/types.ts b/packages/bot/src/transformers/types.ts index 7db31e6b2..7bfde7356 100644 --- a/packages/bot/src/transformers/types.ts +++ b/packages/bot/src/transformers/types.ts @@ -31,6 +31,7 @@ import type { DiscordScheduledEventRecurrenceRuleNWeekday, DiscordScheduledEventRecurrenceRuleWeekday, DiscordSkuType, + DiscordSubscriptionStatus, DiscordTeamMemberRole, DiscordTemplateSerializedSourceGuild, DiscordWebhookEventType, @@ -1741,3 +1742,24 @@ export interface GuildWidgetSettings { channelId?: string enabled: boolean } + +export interface Subscription { + /** ID of the subscription */ + id: bigint + /** ID of the user who is subscribed */ + userId: bigint + /** List of SKUs subscribed to */ + skuIds: bigint[] + /** List of entitlements granted for this subscription */ + entitlementIds: bigint[] + /** Start of the current subscription period */ + currentPeriodStart: number + /** End of the current subscription period */ + currentPeriodEnd: number + /** Current status of the subscription */ + status: DiscordSubscriptionStatus + /** When the subscription was canceled */ + canceledAt: number + /** ISO3166-1 alpha-2 country code of the payment source used to purchase the subscription. Missing unless queried with a private OAuth scope. */ + country?: string +} diff --git a/packages/bot/src/typings.ts b/packages/bot/src/typings.ts index 7cd3bffab..78f5968dc 100644 --- a/packages/bot/src/typings.ts +++ b/packages/bot/src/typings.ts @@ -207,6 +207,9 @@ export interface BotGatewayHandlerOptions { ENTITLEMENT_CREATE: typeof handlers.handleEntitlementCreate ENTITLEMENT_UPDATE: typeof handlers.handleEntitlementUpdate ENTITLEMENT_DELETE: typeof handlers.handleEntitlementDelete + SUBSCRIPTION_CREATE: typeof handlers.handleSubscriptionCreate + SUBSCRIPTION_UPDATE: typeof handlers.handleSubscriptionUpdate + SUBSCRIPTION_DELETE: typeof handlers.handleSubscriptionDelete MESSAGE_POLL_VOTE_ADD: typeof handlers.handleMessagePollVoteAdd MESSAGE_POLL_VOTE_REMOVE: typeof handlers.handleMessagePollVoteRemove } diff --git a/packages/rest/src/manager.ts b/packages/rest/src/manager.ts index def45bbfe..b4db1d71a 100644 --- a/packages/rest/src/manager.ts +++ b/packages/rest/src/manager.ts @@ -41,6 +41,7 @@ import { type DiscordStageInstance, type DiscordSticker, type DiscordStickerPack, + type DiscordSubscription, type DiscordTemplate, type DiscordThreadMember, type DiscordUser, @@ -1629,6 +1630,14 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage return await rest.get(rest.routes.monetization.skus(applicationId)) }, + async listSubscriptions(skuId, options) { + return await rest.get(rest.routes.monetization.subscriptions(skuId, options)) + }, + + async getSubscription(skuId, subscriptionId) { + return await rest.get(rest.routes.monetization.subscription(skuId, subscriptionId)) + }, + preferSnakeCase(enabled: boolean) { const camelizer = enabled ? (x: any) => x : camelize diff --git a/packages/rest/src/routes.ts b/packages/rest/src/routes.ts index 37cff1404..4e518828e 100644 --- a/packages/rest/src/routes.ts +++ b/packages/rest/src/routes.ts @@ -593,6 +593,23 @@ export function createRoutes(): RestRoutes { skus: (applicationId) => { return `/applications/${applicationId}/skus` }, + + subscription: (skuId, subscriptionId) => { + return `/skus/${skuId}/subscriptions/${subscriptionId}` + }, + + subscriptions: (skuId, options) => { + let url = `/skus/${skuId}/subscriptions?` + + if (options) { + if (options.after) url += `after=${options.after}` + if (options.before) url += `&before=${options.before}` + if (options.userId) url += `&user_id=${options.userId}` + if (options.limit) url += `&limit=${options.limit}` + } + + return url + }, }, applicationEmoji(applicationId, emojiId) { diff --git a/packages/rest/src/types.ts b/packages/rest/src/types.ts index dfcdfb8e9..b99ca9539 100644 --- a/packages/rest/src/types.ts +++ b/packages/rest/src/types.ts @@ -86,6 +86,7 @@ import type { // eslint-disable-next-line @typescript-eslint/no-unused-vars DiscordInteraction, DiscordInteractionCallbackResponse, + DiscordSubscription, EditApplication, EditAutoModerationRuleOptions, EditBotMemberOptions, @@ -119,6 +120,7 @@ import type { InteractionResponse, ListArchivedThreads, ListGuildMembers, + ListSkuSubscriptionsOptions, MfaLevels, ModifyApplicationEmoji, ModifyChannel, @@ -3028,6 +3030,18 @@ export interface RestManager { * @param applicationId - The id of the application to get the SKUs */ listSkus: (applicationId: BigString) => Promise + /** + * Returns all subscriptions containing the SKU, filtered by user. + * + * @param skuId - The id of the sku of get the subscriptions for + */ + listSubscriptions: (skuId: BigString, options?: ListSkuSubscriptionsOptions) => Promise> + /** + * Get a subscription by its ID. + * + * @param skuId - The id of the sku of get the subscriptions for + */ + getSubscription: (skuId: BigString, subscriptionId: BigString) => Promise> } export type RequestMethods = 'GET' | 'POST' | 'DELETE' | 'PATCH' | 'PUT' diff --git a/packages/rest/src/typings/routes.ts b/packages/rest/src/typings/routes.ts index f410467ed..92a65f694 100644 --- a/packages/rest/src/typings/routes.ts +++ b/packages/rest/src/typings/routes.ts @@ -13,6 +13,7 @@ import type { InteractionCallbackOptions, ListArchivedThreads, ListGuildMembers, + ListSkuSubscriptionsOptions, } from '@discordeno/types' export interface RestRoutes { @@ -274,6 +275,10 @@ export interface RestRoutes { consumeEntitlement: (applicationId: BigString, entitlementId: BigString) => string /** Route to list the SKUs */ skus: (applicationId: BigString) => string + /** Route to list the SKU subscriptions */ + subscriptions: (skuId: BigString, options?: ListSkuSubscriptionsOptions) => string + /** Route to get a SKU subscription */ + subscription: (skuId: BigString, subscriptionId: BigString) => string } /** Route to list / create an application emoji */ applicationEmojis: (applicationId: BigString) => string diff --git a/packages/types/src/discord.ts b/packages/types/src/discord.ts index 4d78e5ee9..6735d4c02 100644 --- a/packages/types/src/discord.ts +++ b/packages/types/src/discord.ts @@ -3778,6 +3778,38 @@ export enum DiscordSkuType { SubscriptionGroup = 6, } +/** https://discord.com/developers/docs/resources/subscription#subscription-object */ +export interface DiscordSubscription { + /** ID of the subscription */ + id: string + /** ID of the user who is subscribed */ + user_id: string + /** List of SKUs subscribed to */ + sku_ids: string[] + /** List of entitlements granted for this subscription */ + entitlement_ids: string[] + /** Start of the current subscription period */ + current_period_start: string + /** End of the current subscription period */ + current_period_end: string + /** Current status of the subscription */ + status: DiscordSubscriptionStatus + /** When the subscription was canceled */ + canceled_at: string | null + /** ISO3166-1 alpha-2 country code of the payment source used to purchase the subscription. Missing unless queried with a private OAuth scope. */ + country?: string +} + +/** https://discord.com/developers/docs/resources/subscription#subscription-statuses */ +export enum DiscordSubscriptionStatus { + /** Subscription is active and scheduled to renew. */ + Active, + /** Subscription is active but will not renew. */ + Ending, + /** Subscription is inactive and not being charged. */ + Inactive, +} + /** https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-interaction-context-types */ export enum DiscordInteractionContextType { /** Interaction can be used within servers */ diff --git a/packages/types/src/discordeno.ts b/packages/types/src/discordeno.ts index 563dbd88b..58341b8fc 100644 --- a/packages/types/src/discordeno.ts +++ b/packages/types/src/discordeno.ts @@ -1586,3 +1586,15 @@ export interface GetPollAnswerVotes { */ limit?: number } + +/** https://discord.com/developers/docs/resources/subscription#query-string-params */ +export interface ListSkuSubscriptionsOptions { + /** List subscriptions before this ID */ + before?: BigString + /** List subscriptions after this ID */ + after?: BigString + /** Number of results to return (1-100) */ + limit?: number + /** User ID for which to return subscriptions. Required except for OAuth queries. */ + userId?: BigString +} diff --git a/packages/types/src/shared.ts b/packages/types/src/shared.ts index 6be9e09b5..8c50c84b9 100644 --- a/packages/types/src/shared.ts +++ b/packages/types/src/shared.ts @@ -941,6 +941,9 @@ export type GatewayDispatchEventNames = | 'ENTITLEMENT_CREATE' | 'ENTITLEMENT_UPDATE' | 'ENTITLEMENT_DELETE' + | 'SUBSCRIPTION_CREATE' + | 'SUBSCRIPTION_UPDATE' + | 'SUBSCRIPTION_DELETE' | 'MESSAGE_POLL_VOTE_ADD' | 'MESSAGE_POLL_VOTE_REMOVE'