Skip to content

Commit 108943a

Browse files
sdanialrazaJiralitekodiakhq[bot]
authored
feat: add subscriptions (#10541)
* feat: add subscriptions * types: fix fetch options types * fix: correct properties in patch method * chore: requested changes Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> * fix: correct export syntax * chore(Entitlement): mark `ends_at` as nullable` * types(FetchSubscriptionOptions): add missing `cache` option * Revert "types(FetchSubscriptionOptions): add missing `cache` option" This reverts commit ba472bd. * chore(Entitlement): mark `startsTimestamp` as nullable * fix: requested changes * docs(SubscriptionManager): correct return type --------- Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
1 parent 9010b12 commit 108943a

File tree

11 files changed

+301
-6
lines changed

11 files changed

+301
-6
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
'use strict';
2+
3+
const Events = require('../../../util/Events');
4+
5+
module.exports = (client, { d: data }) => {
6+
const subscription = client.application.subscriptions._add(data);
7+
8+
/**
9+
* Emitted whenever a subscription is created.
10+
* @event Client#subscriptionCreate
11+
* @param {Subscription} subscription The subscription that was created
12+
*/
13+
client.emit(Events.SubscriptionCreate, subscription);
14+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use strict';
2+
3+
const Events = require('../../../util/Events');
4+
5+
module.exports = (client, { d: data }) => {
6+
const subscription = client.application.subscriptions._add(data, false);
7+
8+
client.application.subscriptions.cache.delete(subscription.id);
9+
10+
/**
11+
* Emitted whenever a subscription is deleted.
12+
* @event Client#subscriptionDelete
13+
* @param {Subscription} subscription The subscription that was deleted
14+
*/
15+
client.emit(Events.SubscriptionDelete, subscription);
16+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use strict';
2+
3+
const Events = require('../../../util/Events');
4+
5+
module.exports = (client, { d: data }) => {
6+
const oldSubscription = client.application.subscriptions.cache.get(data.id)?._clone() ?? null;
7+
const newSubscription = client.application.subscriptions._add(data);
8+
9+
/**
10+
* Emitted whenever a subscription is updated - i.e. when a user's subscription renews.
11+
* @event Client#subscriptionUpdate
12+
* @param {?Subscription} oldSubscription The subscription before the update
13+
* @param {Subscription} newSubscription The subscription after the update
14+
*/
15+
client.emit(Events.SubscriptionUpdate, oldSubscription, newSubscription);
16+
};

packages/discord.js/src/client/websocket/handlers/index.js

+3
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ const handlers = Object.fromEntries([
5252
['STAGE_INSTANCE_CREATE', require('./STAGE_INSTANCE_CREATE')],
5353
['STAGE_INSTANCE_DELETE', require('./STAGE_INSTANCE_DELETE')],
5454
['STAGE_INSTANCE_UPDATE', require('./STAGE_INSTANCE_UPDATE')],
55+
['SUBSCRIPTION_CREATE', require('./SUBSCRIPTION_CREATE')],
56+
['SUBSCRIPTION_DELETE', require('./SUBSCRIPTION_DELETE')],
57+
['SUBSCRIPTION_UPDATE', require('./SUBSCRIPTION_UPDATE')],
5558
['THREAD_CREATE', require('./THREAD_CREATE')],
5659
['THREAD_DELETE', require('./THREAD_DELETE')],
5760
['THREAD_LIST_SYNC', require('./THREAD_LIST_SYNC')],

packages/discord.js/src/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ exports.ReactionManager = require('./managers/ReactionManager');
8383
exports.ReactionUserManager = require('./managers/ReactionUserManager');
8484
exports.RoleManager = require('./managers/RoleManager');
8585
exports.StageInstanceManager = require('./managers/StageInstanceManager');
86+
exports.SubscriptionManager = require('./managers/SubscriptionManager').SubscriptionManager;
8687
exports.ThreadManager = require('./managers/ThreadManager');
8788
exports.ThreadMemberManager = require('./managers/ThreadMemberManager');
8889
exports.UserManager = require('./managers/UserManager');
@@ -193,6 +194,7 @@ exports.SKU = require('./structures/SKU').SKU;
193194
exports.StringSelectMenuOptionBuilder = require('./structures/StringSelectMenuOptionBuilder');
194195
exports.StageChannel = require('./structures/StageChannel');
195196
exports.StageInstance = require('./structures/StageInstance').StageInstance;
197+
exports.Subscription = require('./structures/Subscription').Subscription;
196198
exports.Sticker = require('./structures/Sticker').Sticker;
197199
exports.StickerPack = require('./structures/StickerPack');
198200
exports.Team = require('./structures/Team');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
'use strict';
2+
3+
const { Collection } = require('@discordjs/collection');
4+
const { makeURLSearchParams } = require('@discordjs/rest');
5+
const { Routes } = require('discord-api-types/v10');
6+
const CachedManager = require('./CachedManager');
7+
const { DiscordjsTypeError, ErrorCodes } = require('../errors/index');
8+
const { Subscription } = require('../structures/Subscription');
9+
const { resolveSKUId } = require('../util/Util');
10+
11+
/**
12+
* Manages API methods for subscriptions and stores their cache.
13+
* @extends {CachedManager}
14+
*/
15+
class SubscriptionManager extends CachedManager {
16+
constructor(client, iterable) {
17+
super(client, Subscription, iterable);
18+
}
19+
20+
/**
21+
* The cache of this manager
22+
* @type {Collection<Snowflake, Subscription>}
23+
* @name SubscriptionManager#cache
24+
*/
25+
26+
/**
27+
* Options used to fetch a subscription
28+
* @typedef {BaseFetchOptions} FetchSubscriptionOptions
29+
* @property {SKUResolvable} sku The SKU to fetch the subscription for
30+
* @property {Snowflake} subscriptionId The id of the subscription to fetch
31+
*/
32+
33+
/**
34+
* Options used to fetch subscriptions
35+
* @typedef {Object} FetchSubscriptionsOptions
36+
* @property {Snowflake} [after] Consider only subscriptions after this subscription id
37+
* @property {Snowflake} [before] Consider only subscriptions before this subscription id
38+
* @property {number} [limit] The maximum number of subscriptions to fetch
39+
* @property {SKUResolvable} sku The SKU to fetch subscriptions for
40+
* @property {UserResolvable} user The user to fetch entitlements for
41+
* <warn>If both `before` and `after` are provided, only `before` is respected</warn>
42+
*/
43+
44+
/**
45+
* Fetches subscriptions for this application
46+
* @param {FetchSubscriptionOptions|FetchSubscriptionsOptions} [options={}] Options for fetching the subscriptions
47+
* @returns {Promise<Subscription|Collection<Snowflake, Subscription>>}
48+
*/
49+
async fetch(options = {}) {
50+
if (typeof options !== 'object') throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'options', 'object', true);
51+
52+
const { after, before, cache, limit, sku, subscriptionId, user } = options;
53+
54+
const skuId = resolveSKUId(sku);
55+
56+
if (!skuId) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'sku', 'SKUResolvable');
57+
58+
if (subscriptionId) {
59+
const subscription = await this.client.rest.get(Routes.skuSubscription(skuId, subscriptionId));
60+
61+
return this._add(subscription, cache);
62+
}
63+
64+
const query = makeURLSearchParams({
65+
limit,
66+
user_id: this.client.users.resolveId(user) ?? undefined,
67+
sku_id: skuId,
68+
before,
69+
after,
70+
});
71+
72+
const subscriptions = await this.client.rest.get(Routes.skuSubscriptions(skuId), { query });
73+
74+
return subscriptions.reduce(
75+
(coll, subscription) => coll.set(subscription.id, this._add(subscription, cache)),
76+
new Collection(),
77+
);
78+
}
79+
}
80+
81+
exports.SubscriptionManager = SubscriptionManager;

packages/discord.js/src/structures/ClientApplication.js

+7
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const Application = require('./interfaces/Application');
99
const ApplicationCommandManager = require('../managers/ApplicationCommandManager');
1010
const ApplicationEmojiManager = require('../managers/ApplicationEmojiManager');
1111
const { EntitlementManager } = require('../managers/EntitlementManager');
12+
const { SubscriptionManager } = require('../managers/SubscriptionManager');
1213
const ApplicationFlagsBitField = require('../util/ApplicationFlagsBitField');
1314
const { resolveImage } = require('../util/DataResolver');
1415
const PermissionsBitField = require('../util/PermissionsBitField');
@@ -44,6 +45,12 @@ class ClientApplication extends Application {
4445
* @type {EntitlementManager}
4546
*/
4647
this.entitlements = new EntitlementManager(this.client);
48+
49+
/**
50+
* The subscription manager for this application
51+
* @type {SubscriptionManager}
52+
*/
53+
this.subscriptions = new SubscriptionManager(this.client);
4754
}
4855

4956
_patch(data) {

packages/discord.js/src/structures/Entitlement.js

+2-6
Original file line numberDiff line numberDiff line change
@@ -73,21 +73,19 @@ class Entitlement extends Base {
7373
if ('starts_at' in data) {
7474
/**
7575
* The timestamp at which this entitlement is valid
76-
* <info>This is only `null` for test entitlements</info>
7776
* @type {?number}
7877
*/
79-
this.startsTimestamp = Date.parse(data.starts_at);
78+
this.startsTimestamp = data.starts_at ? Date.parse(data.starts_at) : null;
8079
} else {
8180
this.startsTimestamp ??= null;
8281
}
8382

8483
if ('ends_at' in data) {
8584
/**
8685
* The timestamp at which this entitlement is no longer valid
87-
* <info>This is only `null` for test entitlements</info>
8886
* @type {?number}
8987
*/
90-
this.endsTimestamp = Date.parse(data.ends_at);
88+
this.endsTimestamp = data.ends_at ? Date.parse(data.ends_at) : null;
9189
} else {
9290
this.endsTimestamp ??= null;
9391
}
@@ -114,7 +112,6 @@ class Entitlement extends Base {
114112

115113
/**
116114
* The start date at which this entitlement is valid
117-
* <info>This is only `null` for test entitlements</info>
118115
* @type {?Date}
119116
*/
120117
get startsAt() {
@@ -123,7 +120,6 @@ class Entitlement extends Base {
123120

124121
/**
125122
* The end date at which this entitlement is no longer valid
126-
* <info>This is only `null` for test entitlements</info>
127123
* @type {?Date}
128124
*/
129125
get endsAt() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
'use strict';
2+
3+
const Base = require('./Base');
4+
5+
/**
6+
* Represents a Subscription
7+
* @extends {Base}
8+
*/
9+
class Subscription extends Base {
10+
constructor(client, data) {
11+
super(client);
12+
13+
/**
14+
* The id of the subscription
15+
* @type {Snowflake}
16+
*/
17+
this.id = data.id;
18+
19+
/**
20+
* The id of the user who subscribed
21+
* @type {Snowflake}
22+
*/
23+
this.userId = data.user_id;
24+
25+
this._patch(data);
26+
}
27+
28+
_patch(data) {
29+
/**
30+
* The SKU ids subscribed to
31+
* @type {Snowflake[]}
32+
*/
33+
this.skuIds = data.sku_ids;
34+
35+
/**
36+
* The entitlement ids granted for this subscription
37+
* @type {Snowflake[]}
38+
*/
39+
this.entitlementIds = data.entitlement_ids;
40+
41+
/**
42+
* The timestamp the current subscription period will start at
43+
* @type {number}
44+
*/
45+
this.currentPeriodStartTimestamp = Date.parse(data.current_period_start);
46+
47+
/**
48+
* The timestamp the current subscription period will end at
49+
* @type {number}
50+
*/
51+
this.currentPeriodEndTimestamp = Date.parse(data.current_period_end);
52+
53+
/**
54+
* The current status of the subscription
55+
* @type {SubscriptionStatus}
56+
*/
57+
this.status = data.status;
58+
59+
if ('canceled_at' in data) {
60+
/**
61+
* The timestamp of when the subscription was canceled
62+
* @type {?number}
63+
*/
64+
this.canceledTimestamp = data.canceled_at ? Date.parse(data.canceled_at) : null;
65+
} else {
66+
this.canceledTimestamp ??= null;
67+
}
68+
69+
if ('country' in data) {
70+
/**
71+
* ISO 3166-1 alpha-2 country code of the payment source used to purchase the subscription.
72+
* Missing unless queried with a private OAuth scope.
73+
* @type {?string}
74+
*/
75+
this.country = data.country;
76+
} else {
77+
this.country ??= null;
78+
}
79+
}
80+
81+
/**
82+
* The time the subscription was canceled
83+
* @type {?Date}
84+
* @readonly
85+
*/
86+
get canceledAt() {
87+
return this.canceledTimestamp && new Date(this.canceledTimestamp);
88+
}
89+
90+
/**
91+
* The time the current subscription period will start at
92+
* @type {Date}
93+
* @readonly
94+
*/
95+
get currentPeriodStartAt() {
96+
return new Date(this.currentPeriodStartTimestamp);
97+
}
98+
99+
/**
100+
* The time the current subscription period will end at
101+
* @type {Date}
102+
* @readonly
103+
*/
104+
get currentPeriodEndAt() {
105+
return new Date(this.currentPeriodEndTimestamp);
106+
}
107+
}
108+
109+
exports.Subscription = Subscription;

packages/discord.js/src/util/Events.js

+6
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@
6464
* @property {string} StageInstanceCreate stageInstanceCreate
6565
* @property {string} StageInstanceDelete stageInstanceDelete
6666
* @property {string} StageInstanceUpdate stageInstanceUpdate
67+
* @property {string} SubscriptionCreate subscriptionCreate
68+
* @property {string} SubscriptionUpdate subscriptionUpdate
69+
* @property {string} SubscriptionDelete subscriptionDelete
6770
* @property {string} ThreadCreate threadCreate
6871
* @property {string} ThreadDelete threadDelete
6972
* @property {string} ThreadListSync threadListSync
@@ -147,6 +150,9 @@ module.exports = {
147150
StageInstanceCreate: 'stageInstanceCreate',
148151
StageInstanceDelete: 'stageInstanceDelete',
149152
StageInstanceUpdate: 'stageInstanceUpdate',
153+
SubscriptionCreate: 'subscriptionCreate',
154+
SubscriptionUpdate: 'subscriptionUpdate',
155+
SubscriptionDelete: 'subscriptionDelete',
150156
ThreadCreate: 'threadCreate',
151157
ThreadDelete: 'threadDelete',
152158
ThreadListSync: 'threadListSync',

0 commit comments

Comments
 (0)