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

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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.

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.
"""
# 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
29 changes: 27 additions & 2 deletions disnake/sku.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
from .enums import SKUType, try_enum
from .flags import SKUFlags
from .mixins import Hashable
from .subscription import Subscription
from .utils import snowflake_time

if TYPE_CHECKING:
from .state import ConnectionState
from .types.sku import SKU as SKUPayload


Expand Down Expand Up @@ -56,9 +58,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 +84,25 @@ 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):
"""|coro|

Retrieve all the subscriptions for this SKU.
"""
...

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)
122 changes: 122 additions & 0 deletions disnake/subscription.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# SPDX-License-Identifier: MIT

from __future__ import annotations

import datetime
from typing import TYPE_CHECKING, List, Optional

from .enums import SubscriptionStatus, try_enum
from .mixins import Hashable
from .utils import parse_time, snowflake_time

if TYPE_CHECKING:
from .state import ConnectionState
from .types.subscription import Subscription as SubscriptionPayload
from .user import User

__all__ = ("Subscription",)


class Subscription(Hashable):
"""Represents a subscription.

This can only be retrieved using :meth:`SKU.subscriptions` or :meth:`SKU.fetch_subscription`.

.. warning::
:class:`Subscription`\\s should not be used to grant perks. Use :class:`Entitlement`\\s as a way of whether a user should have access to a specific :class:`SKU`.

.. note::
Some subscriptions may have been canceled already; consider using :meth:`is_canceled` to check whether a given subscription was canceled.

.. collapse:: operations

.. describe:: x == y

Checks if two :class:`Subscription`\\s are equal.

.. describe:: x != y

Checks if two :class:`Subscription`\\s are not equal.

.. describe:: hash(x)

Returns the subscription's hash.

.. versionadded:: 2.10

Attributes
----------
id: :class:`int`
The subscription's ID.
user_id: :class:`int`
The ID of the user who is subscribed to the :attr:`sku_ids`.

See also :attr:`user`.
sku_ids: List[:class:`int`]
The ID of the SKUs the user is subscribed to.
renewal_sku_ids: List[:class:`int`]
The ID of the SKUs that will be renewed at the start of the new period.
entitlement_ids: List[:class:`int`]
The ID of the entitlements the user has.
current_period_start: :class:`datetime.datetime`
The time at which the current period for the given subscription started.
current_period_end: :class:`datetime.datetime`
The time at which the current period for the given subscription will end.
status: :class:`SubscriptionStatus`
The current status of the given subscription.
canceled_at: Optional[:class:`datetime.datetime`]
The time at which the subscription was canceled.

See also :attr:`is_canceled`.
"""

__slots__ = (
"_state",
"id",
"user_id",
"sku_ids",
"entitlement_ids",
"renewal_sku_ids",
"current_period_start",
"current_period_end",
"status",
"canceled_at",
)

def __init__(self, *, data: SubscriptionPayload, state: ConnectionState) -> None:
self._state: ConnectionState = state

self.id: int = int(data["id"])
self.user_id: int = int(data["user_id"])
self.sku_ids: List[int] = list(map(int, data["sku_ids"]))
self.entitlement_ids: List[int] = list(map(int, data["entitlement_ids"]))
self.renewal_sku_ids: Optional[List[int]] = (
list(map(int, data["renewal_sku_ids"])) if data["renewal_sku_ids"] else None
)
self.current_period_start: datetime.datetime = parse_time(data["current_period_start"])
self.current_period_end: datetime.datetime = parse_time(data["current_period_end"])
self.status: SubscriptionStatus = try_enum(SubscriptionStatus, data["status"])
self.canceled_at: Optional[datetime.datetime] = parse_time(data["canceled_at"])

@property
def created_at(self) -> datetime.datetime:
""":class:`datetime.datetime`: Returns the subscription's creation time in UTC."""
return snowflake_time(self.id)

@property
def user(self) -> Optional[User]:
"""Optional[:class:`User`]: The user who is subscribed to the :attr:`sku_ids`.

Requires the user to be cached.
See also :attr:`user_id`.
"""
return self._state.get_user(self.user_id)

@property
def is_canceled(self) -> bool:
""":class:`bool`: Whether the subscription was canceled,
based on :attr:`canceled_at`.
"""
if self.canceled_at is None:
return False
return True
23 changes: 23 additions & 0 deletions disnake/types/subscription.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# SPDX-License-Identifier: MIT

from typing import List, Literal, Optional, TypedDict

from typing_extensions import NotRequired

from .snowflake import Snowflake

SubscriptionStatus = Literal[0, 1, 2]


class Subscription(TypedDict):
id: Snowflake
user_id: Snowflake
sku_ids: List[Snowflake]
entitlement_ids: List[Snowflake]
renewal_sku_ids: Optional[List[Snowflake]]
current_period_start: str
current_period_end: str
status: SubscriptionStatus
canceled_at: Optional[str]
# this is always missing unless queried with a private OAuth scope.
country: NotRequired[str]
31 changes: 29 additions & 2 deletions docs/api/events.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1562,8 +1562,8 @@ This section documents events related to entitlements, which are used for applic

Called when an entitlement is updated.

This happens e.g. when a user's subscription gets renewed (in which case the
:attr:`Entitlement.ends_at` attribute reflects the new expiration date).
This happens **only** when a user's subscription ends or is cancelled (in which case the
:attr:`Entitlement.ends_at` attribute reflects the expiration date).

.. versionadded:: 2.10

Expand All @@ -1583,6 +1583,33 @@ This section documents events related to entitlements, which are used for applic
:param entitlement: The entitlement that was deleted.
:type entitlement: :class:`Entitlement`

.. function:: on_subscription_create(subscription)

Called when a subscription is created.

.. versionadded:: 2.10

:param subscription: The subscription that was created.
:type subscription: :class:`Subscription`

.. function:: on_subscription_update(subscription)

Called when a subscription is updated.

.. versionadded:: 2.10

:param subscription: The subscription that was updated.
:type subscription: :class:`Subscription`

.. function:: on_subscription_delete(subscription)

Called when a subscription is deleted.

.. versionadded:: 2.10

:param subscription: The subscription that was deleted.
:type subscription: :class:`Subscription`

Enumerations
------------

Expand Down
Loading
Loading