Skip to content

Commit

Permalink
feat: update monetization (Pycord-Development#2438)
Browse files Browse the repository at this point in the history
  • Loading branch information
plun1331 authored and baribarton committed Oct 24, 2024
1 parent 84d8ec5 commit e467a20
Show file tree
Hide file tree
Showing 13 changed files with 426 additions and 13 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ These changes are available on the `master` branch, but have not yet been releas
([#2450](https://github.com/Pycord-Development/pycord/pull/2450))
- Added support for user-installable applications.
([#2409](https://github.com/Pycord-Development/pycord/pull/2409)
- Added support for one-time purchases for Discord monetization.
([#2438](https://github.com/Pycord-Development/pycord/pull/2438))

### Fixed

Expand Down
64 changes: 59 additions & 5 deletions discord/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
from .guild import Guild
from .http import HTTPClient
from .invite import Invite
from .iterators import GuildIterator
from .iterators import EntitlementIterator, GuildIterator
from .mentions import AllowedMentions
from .monetization import SKU, Entitlement
from .object import Object
Expand Down Expand Up @@ -2043,17 +2043,71 @@ async def fetch_skus(self) -> list[SKU]:
data = await self._connection.http.list_skus(self.application_id)
return [SKU(data=s) for s in data]

async def fetch_entitlements(self) -> list[Entitlement]:
async def fetch_entitlements(
self,
user: Snowflake | None = None,
skus: list[Snowflake] | None = None,
before: SnowflakeTime | None = None,
after: SnowflakeTime | None = None,
limit: int | None = 100,
guild: Snowflake | None = None,
exclude_ended: bool = False,
) -> EntitlementIterator:
"""|coro|
Fetches the bot's entitlements.
.. versionadded:: 2.5
Parameters
----------
user: :class:`.abc.Snowflake` | None
Limit the fetched entitlements to entitlements owned by this user.
skus: list[:class:`.abc.Snowflake`] | None
Limit the fetched entitlements to entitlements that are for these SKUs.
before: :class:`.abc.Snowflake` | :class:`datetime.datetime` | None
Retrieves guilds before this date or object.
If a datetime is provided, it is recommended to use a UTC-aware datetime.
If the datetime is naive, it is assumed to be local time.
after: :class:`.abc.Snowflake` | :class:`datetime.datetime` | None
Retrieve guilds after this date or object.
If a datetime is provided, it is recommended to use a UTC-aware datetime.
If the datetime is naive, it is assumed to be local time.
limit: Optional[:class:`int`]
The number of entitlements to retrieve.
If ``None``, retrieves every entitlement, which may be slow.
Defaults to ``100``.
guild: :class:`.abc.Snowflake` | None
Limit the fetched entitlements to entitlements owned by this guild.
exclude_ended: :class:`bool`
Whether to limit the fetched entitlements to those that have not ended.
Defaults to ``False``.
Returns
-------
List[:class:`.Entitlement`]
The bot's entitlements.
The application's entitlements.
Raises
------
:exc:`HTTPException`
Retrieving the entitlements failed.
"""
return EntitlementIterator(
self._connection,
user_id=user.id,
sku_ids=[sku.id for sku in skus],
before=before,
after=after,
limit=limit,
guild_id=guild.id,
exclude_ended=exclude_ended,
)

@property
def store_url(self) -> str:
""":class:`str`: The URL that leads to the application's store page for monetization.
.. versionadded:: 2.6
"""
data = await self._connection.http.list_entitlements(self.application_id)
return [Entitlement(data=e, state=self._connection) for e in data]
return f"https://discord.com/application-directory/{self.application_id}/store"
7 changes: 7 additions & 0 deletions discord/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ class Button(Component):
The label of the button, if any.
emoji: Optional[:class:`PartialEmoji`]
The emoji of the button, if available.
sku_id: Optional[:class:`int`]
The ID of the SKU this button refers to.
"""

__slots__: tuple[str, ...] = (
Expand All @@ -243,6 +245,7 @@ class Button(Component):
"disabled",
"label",
"emoji",
"sku_id",
)

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

def to_dict(self) -> ButtonComponentPayload:
payload = {
Expand All @@ -276,6 +280,9 @@ def to_dict(self) -> ButtonComponentPayload:
if self.emoji:
payload["emoji"] = self.emoji.to_dict()

if self.sku_id:
payload["sku_id"] = self.sku_id

return payload # type: ignore


Expand Down
10 changes: 10 additions & 0 deletions discord/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,7 @@ class ButtonStyle(Enum):
success = 3
danger = 4
link = 5
premium = 6

# Aliases
blurple = 1
Expand Down Expand Up @@ -1005,13 +1006,22 @@ class ReactionType(Enum):
class SKUType(Enum):
"""The SKU type"""

durable = 2
consumable = 3
subscription = 5
subscription_group = 6


class EntitlementType(Enum):
"""The entitlement type"""

purchase = 1
premium_subscription = 2
developer_gift = 3
test_mode_purchase = 4
free_purchase = 5
user_gift = 6
premium_purchase = 7
application_subscription = 8


Expand Down
56 changes: 56 additions & 0 deletions discord/guild.py
Original file line number Diff line number Diff line change
Expand Up @@ -4070,3 +4070,59 @@ async def create_test_entitlement(self, sku: Snowflake) -> Entitlement:
}
data = await self._state.http.create_test_entitlement(self.id, payload)
return Entitlement(data=data, state=self._state)

async def fetch_entitlements(
self,
skus: list[Snowflake] | None = None,
before: SnowflakeTime | None = None,
after: SnowflakeTime | None = None,
limit: int | None = 100,
exclude_ended: bool = False,
) -> EntitlementIterator:
"""|coro|
Fetches this guild's entitlements.
This is identical to :meth:`Client.fetch_entitlements` with the ``guild`` parameter.
.. versionadded:: 2.6
Parameters
----------
skus: list[:class:`.abc.Snowflake`] | None
Limit the fetched entitlements to entitlements that are for these SKUs.
before: :class:`.abc.Snowflake` | :class:`datetime.datetime` | None
Retrieves guilds before this date or object.
If a datetime is provided, it is recommended to use a UTC-aware datetime.
If the datetime is naive, it is assumed to be local time.
after: :class:`.abc.Snowflake` | :class:`datetime.datetime` | None
Retrieve guilds after this date or object.
If a datetime is provided, it is recommended to use a UTC-aware datetime.
If the datetime is naive, it is assumed to be local time.
limit: Optional[:class:`int`]
The number of entitlements to retrieve.
If ``None``, retrieves every entitlement, which may be slow.
Defaults to ``100``.
exclude_ended: :class:`bool`
Whether to limit the fetched entitlements to those that have not ended.
Defaults to ``False``.
Returns
-------
List[:class:`.Entitlement`]
The application's entitlements.
Raises
------
:exc:`HTTPException`
Retrieving the entitlements failed.
"""
return EntitlementIterator(
self._state,
sku_ids=[sku.id for sku in skus],
before=before,
after=after,
limit=limit,
guild_id=self.id,
exclude_ended=exclude_ended,
)
37 changes: 37 additions & 0 deletions discord/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -2957,12 +2957,49 @@ def list_skus(
def list_entitlements(
self,
application_id: Snowflake,
*,
user_id: Snowflake | None = None,
sku_ids: list[Snowflake] | None = None,
before: Snowflake | None = None,
after: Snowflake | None = None,
limit: int | None = None,
guild_id: Snowflake | None = None,
exclude_ended: bool | None = None,
) -> Response[list[monetization.Entitlement]]:
params: dict[str, Any] = {}
if user_id is not None:
params["user_id"] = user_id
if sku_ids is not None:
params["sku_ids"] = ",".join(sku_ids)
if before is not None:
params["before"] = before
if after is not None:
params["after"] = after
if limit is not None:
params["limit"] = limit
if guild_id is not None:
params["guild_id"] = guild_id
if exclude_ended is not None:
params["exclude_ended"] = exclude_ended

r = Route(
"GET",
"/applications/{application_id}/entitlements",
application_id=application_id,
)
return self.request(r, params=params)

def consume_entitlement(
self,
application_id: Snowflake,
entitlement_id: Snowflake,
) -> Response[None]:
r = Route(
"POST",
"/applications/{application_id}/entitlements/{entitlement_id}/consume",
application_id=application_id,
entitlement_id=entitlement_id,
)
return self.request(r)

def create_test_entitlement(
Expand Down
6 changes: 6 additions & 0 deletions discord/interactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1229,10 +1229,16 @@ async def send_modal(self, modal: Modal) -> Interaction:
self._parent._state.store_modal(modal, self._parent.user.id)
return self._parent

@utils.deprecated("a button with type ButtonType.premium", "2.6")
async def premium_required(self) -> Interaction:
"""|coro|
Responds to this interaction by sending a premium required message.
.. deprecated:: 2.6
A button with type :attr:`ButtonType.premium` should be used instead.
Raises
------
HTTPException
Expand Down
84 changes: 84 additions & 0 deletions discord/iterators.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@

from .audit_logs import AuditLogEntry
from .errors import NoMoreItems
from .monetization import Entitlement
from .object import Object
from .utils import maybe_coroutine, snowflake_time, time_snowflake

Expand All @@ -50,6 +51,7 @@
"GuildIterator",
"MemberIterator",
"ScheduledEventSubscribersIterator",
"EntitlementIterator",
)

if TYPE_CHECKING:
Expand Down Expand Up @@ -964,6 +966,7 @@ def user_from_payload(self, data):
async def fill_subs(self):
if not self._get_retrieve():
return

before = self.before.id if self.before else None
after = self.after.id if self.after else None
data = await self.get_subscribers(
Expand All @@ -988,3 +991,84 @@ async def fill_subs(self):
await self.subscribers.put(self.member_from_payload(element))
else:
await self.subscribers.put(self.user_from_payload(element))


class EntitlementIterator(_AsyncIterator["Entitlement"]):
def __init__(
self,
state,
user_id: int | None = None,
sku_ids: list[int] | None = None,
before: datetime.datetime | Object | None = None,
after: datetime.datetime | Object | None = None,
limit: int | None = None,
guild_id: int | None = None,
exclude_ended: bool | None = None,
):
self.user_id = user_id
self.sku_ids = sku_ids

if isinstance(before, datetime.datetime):
before = Object(id=time_snowflake(before, high=False))
if isinstance(after, datetime.datetime):
after = Object(id=time_snowflake(after, high=True))

self.before = before
self.after = after
self.limit = limit
self.guild_id = guild_id
self.exclude_ended = exclude_ended

self.state = state
self.get_entitlements = state.http.list_entitlements
self.entitlements = asyncio.Queue()

async def next(self) -> BanEntry:
if self.entitlements.empty():
await self.fill_entitlements()

try:
return self.entitlements.get_nowait()
except asyncio.QueueEmpty:
raise NoMoreItems()

def _get_retrieve(self):
l = self.limit
if l is None or l > 100:
r = 100
else:
r = l
self.retrieve = r
return r > 0

async def fill_entitlements(self):
if not self._get_retrieve():
return

before = self.before.id if self.before else None
after = self.after.id if self.after else None
data = await self.get_entitlements(
self.state.application_id,
before=before,
after=after,
limit=self.retrieve,
user_id=self.user_id,
guild_id=self.guild_id,
sku_ids=self.sku_ids,
exclude_ended=self.exclude_ended,
)

if not data:
# no data, terminate
return

if self.limit:
self.limit -= self.retrieve

if len(data) < 100:
self.limit = 0 # terminate loop

self.after = Object(id=int(data[-1]["id"]))

for element in reversed(data):
await self.entitlements.put(Entitlement(data=element, state=self.state))
Loading

0 comments on commit e467a20

Please sign in to comment.