Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement subscriptions and update the premium apps API #1258

Merged
merged 19 commits into from
Dec 29, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions changelog/1113.feature.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Support application subscriptions and one-time purchases (see the :ddocs:`official docs <monetization/overview>` for more info).
- New types: :class:`SKU`, :class:`Entitlement`.
- New types: :class:`SKU`, :class:`Entitlement`, :class:`Subscription`.
- New :attr:`Interaction.entitlements` attribute, and :meth:`InteractionResponse.require_premium` response type.
- New events: :func:`on_entitlement_create`, :func:`on_entitlement_update`, :func:`on_entitlement_delete`.
- New events: :func:`on_entitlement_create`, :func:`on_entitlement_update`, :func:`on_entitlement_delete`, :func:`on_subscription_create`, :func:`on_subscription_update` and :func:`on_subscription_delete`.
- New :class:`Client` methods: :meth:`~Client.skus`, :meth:`~Client.entitlements`, :meth:`~Client.fetch_entitlement`, :meth:`~Client.create_entitlement`.
4 changes: 2 additions & 2 deletions changelog/1186.feature.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Support application subscriptions and one-time purchases (see the :ddocs:`official docs <monetization/overview>` for more info).
- New types: :class:`SKU`, :class:`Entitlement`.
- New types: :class:`SKU`, :class:`Entitlement`, :class:`Subscription`.
- New :attr:`Interaction.entitlements` attribute, and :meth:`InteractionResponse.require_premium` response type.
- New events: :func:`on_entitlement_create`, :func:`on_entitlement_update`, :func:`on_entitlement_delete`.
- New events: :func:`on_entitlement_create`, :func:`on_entitlement_update`, :func:`on_entitlement_delete`, :func:`on_subscription_create`, :func:`on_subscription_update` and :func:`on_subscription_delete`.
- New :class:`Client` methods: :meth:`~Client.skus`, :meth:`~Client.entitlements`, :meth:`~Client.fetch_entitlement`, :meth:`~Client.create_entitlement`.
4 changes: 2 additions & 2 deletions changelog/1249.feature.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Support application subscriptions and one-time purchases (see the :ddocs:`official docs <monetization/overview>` for more info).
- New types: :class:`SKU`, :class:`Entitlement`.
- New types: :class:`SKU`, :class:`Entitlement`, :class:`Subscription`.
- New :attr:`Interaction.entitlements` attribute, and :meth:`InteractionResponse.require_premium` response type.
- New events: :func:`on_entitlement_create`, :func:`on_entitlement_update`, :func:`on_entitlement_delete`.
- New events: :func:`on_entitlement_create`, :func:`on_entitlement_update`, :func:`on_entitlement_delete`, :func:`on_subscription_create`, :func:`on_subscription_update` and :func:`on_subscription_delete`.
- New :class:`Client` methods: :meth:`~Client.skus`, :meth:`~Client.entitlements`, :meth:`~Client.fetch_entitlement`, :meth:`~Client.create_entitlement`.
5 changes: 5 additions & 0 deletions changelog/1257.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Support application subscriptions and one-time purchases (see the :ddocs:`official docs <monetization/overview>` for more info).
- New types: :class:`SKU`, :class:`Entitlement`, :class:`Subscription`.
- New :attr:`Interaction.entitlements` attribute, and :meth:`InteractionResponse.require_premium` response type.
- New events: :func:`on_entitlement_create`, :func:`on_entitlement_update`, :func:`on_entitlement_delete`, :func:`on_subscription_create`, :func:`on_subscription_update` and :func:`on_subscription_delete`.
- New :class:`Client` methods: :meth:`~Client.skus`, :meth:`~Client.entitlements`, :meth:`~Client.fetch_entitlement`, :meth:`~Client.create_entitlement`.
1 change: 1 addition & 0 deletions disnake/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
from .sku import *
from .stage_instance import *
from .sticker import *
from .subscription import *
from .team import *
from .template import *
from .threads import *
Expand Down
2 changes: 1 addition & 1 deletion disnake/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3135,7 +3135,7 @@ async def skus(self) -> List[SKU]:
The list of SKUs.
"""
data = await self.http.get_skus(self.application_id)
return [SKU(data=d) for d in data]
return [SKU(data=d, state=self._connection) for d in data]

def entitlements(
self,
Expand Down
2 changes: 1 addition & 1 deletion disnake/entitlement.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ class Entitlement(Hashable):
Set to ``None`` when this is a test entitlement.
ends_at: Optional[:class:`datetime.datetime`]
The time at which the entitlement stops being active.
Set to ``None`` when this is a test entitlement.
Set to ``None`` when this is a test entitlement or when this is an indefinite entitlement.
Snipy7374 marked this conversation as resolved.
Show resolved Hide resolved

You can use :meth:`is_active` to check whether this entitlement is still active.
"""
Expand Down
19 changes: 19 additions & 0 deletions disnake/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"OnboardingPromptType",
"SKUType",
"EntitlementType",
"SubscriptionStatus",
"PollLayoutType",
"VoiceChannelEffectAnimationType",
"MessageReferenceType",
Expand Down Expand Up @@ -1322,6 +1323,18 @@ class Event(Enum):
entitlement_delete = "entitlement_delete"
"""Called when a user's entitlement is deleted.
Represents the :func:`on_entitlement_delete` event."""
subscription_create = "subscription_create"
"""Called when a subscription for a premium app is created.
Represents the :func:`on_subscription_create` event.
"""
subscription_update = "subscription_update"
"""Called when a subscription for a premium app is updated.
Represents the :func:`on_subscription_update` event.
"""
subscription_delete = "subscription_delete"
"""Called when a subscription for a premium app is deleted.
Represents the :func:`on_subscription_delete` event.
"""
Snipy7374 marked this conversation as resolved.
Show resolved Hide resolved
# ext.commands events
command = "command"
"""Called when a command is found and is about to be invoked.
Expand Down Expand Up @@ -1407,6 +1420,12 @@ class EntitlementType(Enum):
application_subscription = 8


class SubscriptionStatus(Enum):
active = 0
ending = 1
inactive = 2


class PollLayoutType(Enum):
default = 1

Expand Down
41 changes: 41 additions & 0 deletions disnake/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
role,
sku,
sticker,
subscription,
template,
threads,
user,
Expand Down Expand Up @@ -2411,6 +2412,46 @@ def get_entitlement(
)
)

def get_subscriptions(
self,
sku_id: Snowflake,
*,
before: Optional[Snowflake] = None,
after: Optional[Snowflake] = None,
limit: int = 50,
user_id: Optional[Snowflake] = None,
) -> Response[List[subscription.Subscription]]:
params: Dict[str, Any] = {
"limit": limit,
}
if before is not None:
params["before"] = before
if after is not None:
params["after"] = after
if user_id is not None:
params["user_id"] = user_id

return self.request(
Route(
"GET",
"/skus/{sku_id}/subscriptions",
sku_id=sku_id,
),
params=params,
)

def get_subscription(
self, sku_id: Snowflake, subscription_id: int
) -> Response[subscription.Subscription]:
return self.request(
Route(
"GET",
"/skus/{sku_id}/subscriptions/{subscription_id}",
sku_id=sku_id,
subscription_id=subscription_id,
)
)

def create_test_entitlement(
self,
application_id: Snowflake,
Expand Down
100 changes: 100 additions & 0 deletions disnake/iterators.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from .guild_scheduled_event import GuildScheduledEvent
from .integrations import PartialIntegration
from .object import Object
from .subscription import Subscription
from .threads import Thread
from .utils import maybe_coroutine, snowflake_time, time_snowflake

Expand All @@ -39,6 +40,7 @@
"MemberIterator",
"GuildScheduledEventUserIterator",
"EntitlementIterator",
"SubscriptionIterator",
"PollAnswerIterator",
)

Expand All @@ -60,6 +62,7 @@
GuildScheduledEventUser as GuildScheduledEventUserPayload,
)
from .types.message import Message as MessagePayload
from .types.subscription import Subscription as SubscriptionPayload
from .types.threads import Thread as ThreadPayload
from .types.user import PartialUser as PartialUserPayload
from .user import User
Expand Down Expand Up @@ -1147,6 +1150,103 @@ async def _after_strategy(self, retrieve: int) -> List[EntitlementPayload]:
return data


class SubscriptionIterator(_AsyncIterator["Subscription"]):
def __init__(
self,
sku_id: int,
*,
user_id: Optional[int] = None,
state: ConnectionState,
limit: Optional[int] = None,
before: Optional[Union[Snowflake, datetime.datetime]] = None,
after: Optional[Union[Snowflake, datetime.datetime]] = None,
) -> None:
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.sku_id: int = sku_id
self.user_id: Optional[int] = user_id
self.limit: Optional[int] = limit
self.before: Optional[Snowflake] = before
self.after: Snowflake = after or OLDEST_OBJECT

self._state: ConnectionState = state
self.request = self._state.http.get_subscriptions
self.subscriptions: asyncio.Queue[Subscription] = asyncio.Queue()

self._filter: Optional[Callable[[SubscriptionPayload], bool]] = None
if self.before:
self._strategy = self._before_strategy
if self.after != OLDEST_OBJECT:
self._filter = lambda s: int(s["id"]) > self.after.id
else:
self._strategy = self._after_strategy

async def next(self) -> Subscription:
if self.subscriptions.empty():
await self._fill()

try:
return self.subscriptions.get_nowait()
except asyncio.QueueEmpty:
raise NoMoreItems from None

def _get_retrieve(self) -> bool:
limit = self.limit
if limit is None or limit > 100:
retrieve = 100
else:
retrieve = limit
self.retrieve: int = retrieve
return retrieve > 0

async def _fill(self) -> None:
if not self._get_retrieve():
return

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

if self._filter:
data = filter(self._filter, data)

for subscription in data:
await self.subscriptions.put(Subscription(data=subscription, state=self._state))

async def _before_strategy(self, retrieve: int) -> List[SubscriptionPayload]:
before = self.before.id if self.before else None
data = await self.request(
self.sku_id,
before=before,
limit=retrieve,
user_id=self.user_id,
)

if len(data):
if self.limit is not None:
self.limit -= retrieve
self.before = Object(id=int(data[-1]["id"]))
shiftinv marked this conversation as resolved.
Show resolved Hide resolved
return data

async def _after_strategy(self, retrieve: int) -> List[SubscriptionPayload]:
after = self.after.id
data = await self.request(
self.sku_id,
after=after,
limit=retrieve,
user_id=self.user_id,
)

if len(data):
if self.limit is not None:
self.limit -= retrieve
self.after = Object(id=int(data[-1]["id"]))
return data


class PollAnswerIterator(_AsyncIterator[Union["User", "Member"]]):
def __init__(
self,
Expand Down
77 changes: 74 additions & 3 deletions disnake/sku.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@
from __future__ import annotations

import datetime
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Optional

from .enums import SKUType, try_enum
from .flags import SKUFlags
from .iterators import SubscriptionIterator
from .mixins import Hashable
from .subscription import Subscription
from .utils import snowflake_time

if TYPE_CHECKING:
from .abc import Snowflake, SnowflakeTime
from .state import ConnectionState
from .types.sku import SKU as SKUPayload


Expand Down Expand Up @@ -56,9 +60,10 @@ class SKU(Hashable):
The SKU's URL slug, system-generated based on :attr:`name`.
"""

__slots__ = ("id", "type", "application_id", "name", "slug", "_flags")
__slots__ = ("_state", "id", "type", "application_id", "name", "slug", "_flags")

def __init__(self, *, data: SKUPayload) -> None:
def __init__(self, *, data: SKUPayload, state: ConnectionState) -> None:
self._state: ConnectionState = state
self.id: int = int(data["id"])
self.type: SKUType = try_enum(SKUType, data["type"])
self.application_id: int = int(data["application_id"])
Expand All @@ -81,3 +86,69 @@ def created_at(self) -> datetime.datetime:
def flags(self) -> SKUFlags:
""":class:`SKUFlags`: Returns the SKU's flags."""
return SKUFlags._from_value(self._flags)

async def subscriptions(
self,
*,
limit: Optional[int] = 50,
before: Optional[SnowflakeTime] = None,
after: Optional[SnowflakeTime] = None,
user: Optional[Snowflake] = None,
Snipy7374 marked this conversation as resolved.
Show resolved Hide resolved
) -> SubscriptionIterator:
"""|coro|

Retrieves an :class:`.AsyncIterator` that enabled receiving subscriptions for the SKU.

All parameters are optional.

Parameters
----------
limit: Optional[:class:`int`]
The number of subscriptions to retrieve.
If ``None``, retrieves every subscription.
Note, however, that this would make it a slow operation.
Defaults to ``50``.
before: Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`]
Retrieves subscriptions created 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: Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`]
Retrieve subscriptions created 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.
user: Optional[:class:`.abc.Snowflake`]
The user to retrieve subscriptions for.

Raises
------
HTTPException
Retrieving the subscriptions failed.

Yields
------
:class:`.Subscription`
The subscriptions for the given parameters.
"""
return SubscriptionIterator(
self.id,
state=self._state,
limit=limit,
before=before,
after=after,
user_id=user.id if user is not None else None,
)

async def fetch_subscription(self, subscription_id: int, /) -> Subscription:
"""|coro|

Retrieve a subscription for this SKU given its ID.

Raises
------
NotFound
The subscription does not exist.
HTTPException
Retrieving the subscription failed.
"""
data = await self._state.http.get_subscription(self.id, subscription_id)
return Subscription(data=data, state=self._state)
Loading
Loading