Skip to content

Commit abef1ce

Browse files
authored
feat: update monetization (#2438)
1 parent e65c717 commit abef1ce

File tree

13 files changed

+426
-13
lines changed

13 files changed

+426
-13
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ These changes are available on the `master` branch, but have not yet been releas
3030
([#2450](https://github.com/Pycord-Development/pycord/pull/2450))
3131
- Added support for user-installable applications.
3232
([#2409](https://github.com/Pycord-Development/pycord/pull/2409)
33+
- Added support for one-time purchases for Discord monetization.
34+
([#2438](https://github.com/Pycord-Development/pycord/pull/2438))
3335

3436
### Fixed
3537

discord/client.py

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
from .guild import Guild
5050
from .http import HTTPClient
5151
from .invite import Invite
52-
from .iterators import GuildIterator
52+
from .iterators import EntitlementIterator, GuildIterator
5353
from .mentions import AllowedMentions
5454
from .monetization import SKU, Entitlement
5555
from .object import Object
@@ -2043,17 +2043,71 @@ async def fetch_skus(self) -> list[SKU]:
20432043
data = await self._connection.http.list_skus(self.application_id)
20442044
return [SKU(data=s) for s in data]
20452045

2046-
async def fetch_entitlements(self) -> list[Entitlement]:
2046+
async def fetch_entitlements(
2047+
self,
2048+
user: Snowflake | None = None,
2049+
skus: list[Snowflake] | None = None,
2050+
before: SnowflakeTime | None = None,
2051+
after: SnowflakeTime | None = None,
2052+
limit: int | None = 100,
2053+
guild: Snowflake | None = None,
2054+
exclude_ended: bool = False,
2055+
) -> EntitlementIterator:
20472056
"""|coro|
20482057
20492058
Fetches the bot's entitlements.
20502059
20512060
.. versionadded:: 2.5
20522061
2062+
Parameters
2063+
----------
2064+
user: :class:`.abc.Snowflake` | None
2065+
Limit the fetched entitlements to entitlements owned by this user.
2066+
skus: list[:class:`.abc.Snowflake`] | None
2067+
Limit the fetched entitlements to entitlements that are for these SKUs.
2068+
before: :class:`.abc.Snowflake` | :class:`datetime.datetime` | None
2069+
Retrieves guilds before this date or object.
2070+
If a datetime is provided, it is recommended to use a UTC-aware datetime.
2071+
If the datetime is naive, it is assumed to be local time.
2072+
after: :class:`.abc.Snowflake` | :class:`datetime.datetime` | None
2073+
Retrieve guilds after this date or object.
2074+
If a datetime is provided, it is recommended to use a UTC-aware datetime.
2075+
If the datetime is naive, it is assumed to be local time.
2076+
limit: Optional[:class:`int`]
2077+
The number of entitlements to retrieve.
2078+
If ``None``, retrieves every entitlement, which may be slow.
2079+
Defaults to ``100``.
2080+
guild: :class:`.abc.Snowflake` | None
2081+
Limit the fetched entitlements to entitlements owned by this guild.
2082+
exclude_ended: :class:`bool`
2083+
Whether to limit the fetched entitlements to those that have not ended.
2084+
Defaults to ``False``.
2085+
20532086
Returns
20542087
-------
20552088
List[:class:`.Entitlement`]
2056-
The bot's entitlements.
2089+
The application's entitlements.
2090+
2091+
Raises
2092+
------
2093+
:exc:`HTTPException`
2094+
Retrieving the entitlements failed.
2095+
"""
2096+
return EntitlementIterator(
2097+
self._connection,
2098+
user_id=user.id,
2099+
sku_ids=[sku.id for sku in skus],
2100+
before=before,
2101+
after=after,
2102+
limit=limit,
2103+
guild_id=guild.id,
2104+
exclude_ended=exclude_ended,
2105+
)
2106+
2107+
@property
2108+
def store_url(self) -> str:
2109+
""":class:`str`: The URL that leads to the application's store page for monetization.
2110+
2111+
.. versionadded:: 2.6
20572112
"""
2058-
data = await self._connection.http.list_entitlements(self.application_id)
2059-
return [Entitlement(data=e, state=self._connection) for e in data]
2113+
return f"https://discord.com/application-directory/{self.application_id}/store"

discord/components.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,8 @@ class Button(Component):
234234
The label of the button, if any.
235235
emoji: Optional[:class:`PartialEmoji`]
236236
The emoji of the button, if available.
237+
sku_id: Optional[:class:`int`]
238+
The ID of the SKU this button refers to.
237239
"""
238240

239241
__slots__: tuple[str, ...] = (
@@ -243,6 +245,7 @@ class Button(Component):
243245
"disabled",
244246
"label",
245247
"emoji",
248+
"sku_id",
246249
)
247250

248251
__repr_info__: ClassVar[tuple[str, ...]] = __slots__
@@ -259,6 +262,7 @@ def __init__(self, data: ButtonComponentPayload):
259262
self.emoji = PartialEmoji.from_dict(data["emoji"])
260263
except KeyError:
261264
self.emoji = None
265+
self.sku_id: str | None = data.get("sku_id")
262266

263267
def to_dict(self) -> ButtonComponentPayload:
264268
payload = {
@@ -276,6 +280,9 @@ def to_dict(self) -> ButtonComponentPayload:
276280
if self.emoji:
277281
payload["emoji"] = self.emoji.to_dict()
278282

283+
if self.sku_id:
284+
payload["sku_id"] = self.sku_id
285+
279286
return payload # type: ignore
280287

281288

discord/enums.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,7 @@ class ButtonStyle(Enum):
724724
success = 3
725725
danger = 4
726726
link = 5
727+
premium = 6
727728

728729
# Aliases
729730
blurple = 1
@@ -1005,13 +1006,22 @@ class ReactionType(Enum):
10051006
class SKUType(Enum):
10061007
"""The SKU type"""
10071008

1009+
durable = 2
1010+
consumable = 3
10081011
subscription = 5
10091012
subscription_group = 6
10101013

10111014

10121015
class EntitlementType(Enum):
10131016
"""The entitlement type"""
10141017

1018+
purchase = 1
1019+
premium_subscription = 2
1020+
developer_gift = 3
1021+
test_mode_purchase = 4
1022+
free_purchase = 5
1023+
user_gift = 6
1024+
premium_purchase = 7
10151025
application_subscription = 8
10161026

10171027

discord/guild.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4070,3 +4070,59 @@ async def create_test_entitlement(self, sku: Snowflake) -> Entitlement:
40704070
}
40714071
data = await self._state.http.create_test_entitlement(self.id, payload)
40724072
return Entitlement(data=data, state=self._state)
4073+
4074+
async def fetch_entitlements(
4075+
self,
4076+
skus: list[Snowflake] | None = None,
4077+
before: SnowflakeTime | None = None,
4078+
after: SnowflakeTime | None = None,
4079+
limit: int | None = 100,
4080+
exclude_ended: bool = False,
4081+
) -> EntitlementIterator:
4082+
"""|coro|
4083+
4084+
Fetches this guild's entitlements.
4085+
4086+
This is identical to :meth:`Client.fetch_entitlements` with the ``guild`` parameter.
4087+
4088+
.. versionadded:: 2.6
4089+
4090+
Parameters
4091+
----------
4092+
skus: list[:class:`.abc.Snowflake`] | None
4093+
Limit the fetched entitlements to entitlements that are for these SKUs.
4094+
before: :class:`.abc.Snowflake` | :class:`datetime.datetime` | None
4095+
Retrieves guilds before this date or object.
4096+
If a datetime is provided, it is recommended to use a UTC-aware datetime.
4097+
If the datetime is naive, it is assumed to be local time.
4098+
after: :class:`.abc.Snowflake` | :class:`datetime.datetime` | None
4099+
Retrieve guilds after this date or object.
4100+
If a datetime is provided, it is recommended to use a UTC-aware datetime.
4101+
If the datetime is naive, it is assumed to be local time.
4102+
limit: Optional[:class:`int`]
4103+
The number of entitlements to retrieve.
4104+
If ``None``, retrieves every entitlement, which may be slow.
4105+
Defaults to ``100``.
4106+
exclude_ended: :class:`bool`
4107+
Whether to limit the fetched entitlements to those that have not ended.
4108+
Defaults to ``False``.
4109+
4110+
Returns
4111+
-------
4112+
List[:class:`.Entitlement`]
4113+
The application's entitlements.
4114+
4115+
Raises
4116+
------
4117+
:exc:`HTTPException`
4118+
Retrieving the entitlements failed.
4119+
"""
4120+
return EntitlementIterator(
4121+
self._state,
4122+
sku_ids=[sku.id for sku in skus],
4123+
before=before,
4124+
after=after,
4125+
limit=limit,
4126+
guild_id=self.id,
4127+
exclude_ended=exclude_ended,
4128+
)

discord/http.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2957,12 +2957,49 @@ def list_skus(
29572957
def list_entitlements(
29582958
self,
29592959
application_id: Snowflake,
2960+
*,
2961+
user_id: Snowflake | None = None,
2962+
sku_ids: list[Snowflake] | None = None,
2963+
before: Snowflake | None = None,
2964+
after: Snowflake | None = None,
2965+
limit: int | None = None,
2966+
guild_id: Snowflake | None = None,
2967+
exclude_ended: bool | None = None,
29602968
) -> Response[list[monetization.Entitlement]]:
2969+
params: dict[str, Any] = {}
2970+
if user_id is not None:
2971+
params["user_id"] = user_id
2972+
if sku_ids is not None:
2973+
params["sku_ids"] = ",".join(sku_ids)
2974+
if before is not None:
2975+
params["before"] = before
2976+
if after is not None:
2977+
params["after"] = after
2978+
if limit is not None:
2979+
params["limit"] = limit
2980+
if guild_id is not None:
2981+
params["guild_id"] = guild_id
2982+
if exclude_ended is not None:
2983+
params["exclude_ended"] = exclude_ended
2984+
29612985
r = Route(
29622986
"GET",
29632987
"/applications/{application_id}/entitlements",
29642988
application_id=application_id,
29652989
)
2990+
return self.request(r, params=params)
2991+
2992+
def consume_entitlement(
2993+
self,
2994+
application_id: Snowflake,
2995+
entitlement_id: Snowflake,
2996+
) -> Response[None]:
2997+
r = Route(
2998+
"POST",
2999+
"/applications/{application_id}/entitlements/{entitlement_id}/consume",
3000+
application_id=application_id,
3001+
entitlement_id=entitlement_id,
3002+
)
29663003
return self.request(r)
29673004

29683005
def create_test_entitlement(

discord/interactions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1229,10 +1229,16 @@ async def send_modal(self, modal: Modal) -> Interaction:
12291229
self._parent._state.store_modal(modal, self._parent.user.id)
12301230
return self._parent
12311231

1232+
@utils.deprecated("a button with type ButtonType.premium", "2.6")
12321233
async def premium_required(self) -> Interaction:
12331234
"""|coro|
1235+
12341236
Responds to this interaction by sending a premium required message.
12351237
1238+
.. deprecated:: 2.6
1239+
1240+
A button with type :attr:`ButtonType.premium` should be used instead.
1241+
12361242
Raises
12371243
------
12381244
HTTPException

discord/iterators.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040

4141
from .audit_logs import AuditLogEntry
4242
from .errors import NoMoreItems
43+
from .monetization import Entitlement
4344
from .object import Object
4445
from .utils import maybe_coroutine, snowflake_time, time_snowflake
4546

@@ -50,6 +51,7 @@
5051
"GuildIterator",
5152
"MemberIterator",
5253
"ScheduledEventSubscribersIterator",
54+
"EntitlementIterator",
5355
)
5456

5557
if TYPE_CHECKING:
@@ -964,6 +966,7 @@ def user_from_payload(self, data):
964966
async def fill_subs(self):
965967
if not self._get_retrieve():
966968
return
969+
967970
before = self.before.id if self.before else None
968971
after = self.after.id if self.after else None
969972
data = await self.get_subscribers(
@@ -988,3 +991,84 @@ async def fill_subs(self):
988991
await self.subscribers.put(self.member_from_payload(element))
989992
else:
990993
await self.subscribers.put(self.user_from_payload(element))
994+
995+
996+
class EntitlementIterator(_AsyncIterator["Entitlement"]):
997+
def __init__(
998+
self,
999+
state,
1000+
user_id: int | None = None,
1001+
sku_ids: list[int] | None = None,
1002+
before: datetime.datetime | Object | None = None,
1003+
after: datetime.datetime | Object | None = None,
1004+
limit: int | None = None,
1005+
guild_id: int | None = None,
1006+
exclude_ended: bool | None = None,
1007+
):
1008+
self.user_id = user_id
1009+
self.sku_ids = sku_ids
1010+
1011+
if isinstance(before, datetime.datetime):
1012+
before = Object(id=time_snowflake(before, high=False))
1013+
if isinstance(after, datetime.datetime):
1014+
after = Object(id=time_snowflake(after, high=True))
1015+
1016+
self.before = before
1017+
self.after = after
1018+
self.limit = limit
1019+
self.guild_id = guild_id
1020+
self.exclude_ended = exclude_ended
1021+
1022+
self.state = state
1023+
self.get_entitlements = state.http.list_entitlements
1024+
self.entitlements = asyncio.Queue()
1025+
1026+
async def next(self) -> BanEntry:
1027+
if self.entitlements.empty():
1028+
await self.fill_entitlements()
1029+
1030+
try:
1031+
return self.entitlements.get_nowait()
1032+
except asyncio.QueueEmpty:
1033+
raise NoMoreItems()
1034+
1035+
def _get_retrieve(self):
1036+
l = self.limit
1037+
if l is None or l > 100:
1038+
r = 100
1039+
else:
1040+
r = l
1041+
self.retrieve = r
1042+
return r > 0
1043+
1044+
async def fill_entitlements(self):
1045+
if not self._get_retrieve():
1046+
return
1047+
1048+
before = self.before.id if self.before else None
1049+
after = self.after.id if self.after else None
1050+
data = await self.get_entitlements(
1051+
self.state.application_id,
1052+
before=before,
1053+
after=after,
1054+
limit=self.retrieve,
1055+
user_id=self.user_id,
1056+
guild_id=self.guild_id,
1057+
sku_ids=self.sku_ids,
1058+
exclude_ended=self.exclude_ended,
1059+
)
1060+
1061+
if not data:
1062+
# no data, terminate
1063+
return
1064+
1065+
if self.limit:
1066+
self.limit -= self.retrieve
1067+
1068+
if len(data) < 100:
1069+
self.limit = 0 # terminate loop
1070+
1071+
self.after = Object(id=int(data[-1]["id"]))
1072+
1073+
for element in reversed(data):
1074+
await self.entitlements.put(Entitlement(data=element, state=self.state))

0 commit comments

Comments
 (0)