Skip to content

Commit

Permalink
feat: implement new Subscription object and endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
Snipy7374 committed Dec 14, 2024
1 parent a95ddaf commit b25792c
Show file tree
Hide file tree
Showing 13 changed files with 318 additions and 11 deletions.
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/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

0 comments on commit b25792c

Please sign in to comment.