mirror of
https://github.com/Rapptz/discord.py.git
synced 2025-04-18 15:06:07 +00:00
Add SKU subscriptions support
This commit is contained in:
parent
0ce75f3f53
commit
58b6929aa5
@ -71,6 +71,7 @@ from .threads import *
|
||||
from .automod import *
|
||||
from .poll import *
|
||||
from .soundboard import *
|
||||
from .subscription import *
|
||||
|
||||
|
||||
class VersionInfo(NamedTuple):
|
||||
|
@ -119,6 +119,7 @@ if TYPE_CHECKING:
|
||||
from .voice_client import VoiceProtocol
|
||||
from .audit_logs import AuditLogEntry
|
||||
from .poll import PollAnswer
|
||||
from .subscription import Subscription
|
||||
|
||||
|
||||
# fmt: off
|
||||
@ -1373,6 +1374,18 @@ class Client:
|
||||
) -> Union[str, bytes]:
|
||||
...
|
||||
|
||||
# Entitlements
|
||||
@overload
|
||||
async def wait_for(
|
||||
self,
|
||||
event: Literal['entitlement_create', 'entitlement_update', 'entitlement_delete'],
|
||||
/,
|
||||
*,
|
||||
check: Optional[Callable[[Entitlement], bool]],
|
||||
timeout: Optional[float] = None,
|
||||
) -> Entitlement:
|
||||
...
|
||||
|
||||
# Guilds
|
||||
|
||||
@overload
|
||||
@ -1781,6 +1794,18 @@ class Client:
|
||||
) -> Coroutine[Any, Any, Tuple[StageInstance, StageInstance]]:
|
||||
...
|
||||
|
||||
# Subscriptions
|
||||
@overload
|
||||
async def wait_for(
|
||||
self,
|
||||
event: Literal['subscription_create', 'subscription_update', 'subscription_delete'],
|
||||
/,
|
||||
*,
|
||||
check: Optional[Callable[[Subscription], bool]],
|
||||
timeout: Optional[float] = None,
|
||||
) -> Subscription:
|
||||
...
|
||||
|
||||
# Threads
|
||||
@overload
|
||||
async def wait_for(
|
||||
|
@ -75,6 +75,7 @@ __all__ = (
|
||||
'EntitlementOwnerType',
|
||||
'PollLayoutType',
|
||||
'VoiceChannelEffectAnimationType',
|
||||
'SubscriptionStatus',
|
||||
)
|
||||
|
||||
|
||||
@ -847,6 +848,12 @@ class VoiceChannelEffectAnimationType(Enum):
|
||||
basic = 1
|
||||
|
||||
|
||||
class SubscriptionStatus(Enum):
|
||||
active = 0
|
||||
ending = 1
|
||||
inactive = 2
|
||||
|
||||
|
||||
def create_unknown_value(cls: Type[E], val: Any) -> E:
|
||||
value_cls = cls._enum_value_cls_ # type: ignore # This is narrowed below
|
||||
name = f'unknown_{val}'
|
||||
|
@ -94,6 +94,7 @@ if TYPE_CHECKING:
|
||||
poll,
|
||||
voice,
|
||||
soundboard,
|
||||
subscription,
|
||||
)
|
||||
from .types.snowflake import Snowflake, SnowflakeList
|
||||
|
||||
@ -2699,6 +2700,49 @@ class HTTPClient:
|
||||
)
|
||||
)
|
||||
|
||||
# Subscriptions
|
||||
|
||||
def list_sku_subscriptions(
|
||||
self,
|
||||
sku_id: Snowflake,
|
||||
before: Optional[Snowflake] = None,
|
||||
after: Optional[Snowflake] = None,
|
||||
limit: Optional[int] = None,
|
||||
user_id: Optional[Snowflake] = None,
|
||||
) -> Response[List[subscription.Subscription]]:
|
||||
params = {}
|
||||
|
||||
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 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_sku_subscription(self, sku_id: Snowflake, subscription_id: Snowflake) -> Response[subscription.Subscription]:
|
||||
return self.request(
|
||||
Route(
|
||||
'GET',
|
||||
'/skus/{sku_id}/subscriptions/{subscription_id}',
|
||||
sku_id=sku_id,
|
||||
subscription_id=subscription_id,
|
||||
)
|
||||
)
|
||||
|
||||
# Misc
|
||||
|
||||
async def get_bot_gateway(self) -> Tuple[int, str]:
|
||||
|
159
discord/sku.py
159
discord/sku.py
@ -25,16 +25,18 @@ DEALINGS IN THE SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from typing import AsyncIterator, Optional, TYPE_CHECKING
|
||||
|
||||
from . import utils
|
||||
from .errors import MissingApplicationID
|
||||
from .enums import try_enum, SKUType, EntitlementType
|
||||
from .flags import SKUFlags
|
||||
from .object import Object
|
||||
from .subscription import Subscription
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from datetime import datetime
|
||||
|
||||
from .abc import SnowflakeTime, Snowflake
|
||||
from .guild import Guild
|
||||
from .state import ConnectionState
|
||||
from .types.sku import (
|
||||
@ -100,6 +102,149 @@ class SKU:
|
||||
""":class:`datetime.datetime`: Returns the sku's creation time in UTC."""
|
||||
return utils.snowflake_time(self.id)
|
||||
|
||||
async def fetch_subscription(self, subscription_id: int, /) -> Subscription:
|
||||
"""|coro|
|
||||
|
||||
Retrieves a :class:`.Subscription` with the specified ID.
|
||||
|
||||
.. versionadded:: 2.5
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
subscription_id: :class:`int`
|
||||
The subscription's ID to fetch from.
|
||||
|
||||
Raises
|
||||
-------
|
||||
NotFound
|
||||
An subscription with this ID does not exist.
|
||||
HTTPException
|
||||
Fetching the subscription failed.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`.Subscription`
|
||||
The subscription you requested.
|
||||
"""
|
||||
data = await self._state.http.get_sku_subscription(self.id, subscription_id)
|
||||
return Subscription(data=data, state=self._state)
|
||||
|
||||
async def subscriptions(
|
||||
self,
|
||||
*,
|
||||
limit: Optional[int] = 50,
|
||||
before: Optional[SnowflakeTime] = None,
|
||||
after: Optional[SnowflakeTime] = None,
|
||||
user: Snowflake,
|
||||
) -> AsyncIterator[Subscription]:
|
||||
"""Retrieves an :term:`asynchronous iterator` of the :class:`.Subscription` that SKU has.
|
||||
|
||||
.. versionadded:: 2.5
|
||||
|
||||
Examples
|
||||
---------
|
||||
|
||||
Usage ::
|
||||
|
||||
async for subscription in sku.subscriptions(limit=100):
|
||||
print(subscription.user_id, subscription.current_period_end)
|
||||
|
||||
Flattening into a list ::
|
||||
|
||||
subscriptions = [subscription async for subscription in sku.subscriptions(limit=100)]
|
||||
# subscriptions is now a list of Subscription...
|
||||
|
||||
All parameters are optional.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
limit: Optional[:class:`int`]
|
||||
The number of subscriptions to retrieve. If ``None``, it retrieves every subscription for this SKU.
|
||||
Note, however, that this would make it a slow operation. Defaults to ``100``.
|
||||
before: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]]
|
||||
Retrieve subscriptions before this date or entitlement.
|
||||
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: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]]
|
||||
Retrieve subscriptions after this date or entitlement.
|
||||
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: :class:`~discord.abc.Snowflake`
|
||||
The user to filter by.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Fetching the subscriptions failed.
|
||||
TypeError
|
||||
Both ``after`` and ``before`` were provided, as Discord does not
|
||||
support this type of pagination.
|
||||
|
||||
Yields
|
||||
--------
|
||||
:class:`.Subscription`
|
||||
The subscription with the SKU.
|
||||
"""
|
||||
|
||||
if before is not None and after is not None:
|
||||
raise TypeError('subscriptions pagination does not support both before and after')
|
||||
|
||||
# This endpoint paginates in ascending order.
|
||||
endpoint = self._state.http.list_sku_subscriptions
|
||||
|
||||
async def _before_strategy(retrieve: int, before: Optional[Snowflake], limit: Optional[int]):
|
||||
before_id = before.id if before else None
|
||||
data = await endpoint(self.id, before=before_id, limit=retrieve, user_id=user.id)
|
||||
|
||||
if data:
|
||||
if limit is not None:
|
||||
limit -= len(data)
|
||||
|
||||
before = Object(id=int(data[0]['id']))
|
||||
|
||||
return data, before, limit
|
||||
|
||||
async def _after_strategy(retrieve: int, after: Optional[Snowflake], limit: Optional[int]):
|
||||
after_id = after.id if after else None
|
||||
data = await endpoint(
|
||||
self.id,
|
||||
after=after_id,
|
||||
limit=retrieve,
|
||||
user_id=user.id,
|
||||
)
|
||||
|
||||
if data:
|
||||
if limit is not None:
|
||||
limit -= len(data)
|
||||
|
||||
after = Object(id=int(data[-1]['id']))
|
||||
|
||||
return data, after, limit
|
||||
|
||||
if isinstance(before, datetime):
|
||||
before = Object(id=utils.time_snowflake(before, high=False))
|
||||
if isinstance(after, datetime):
|
||||
after = Object(id=utils.time_snowflake(after, high=True))
|
||||
|
||||
if before:
|
||||
strategy, state = _before_strategy, before
|
||||
else:
|
||||
strategy, state = _after_strategy, after
|
||||
|
||||
while True:
|
||||
retrieve = 100 if limit is None else min(limit, 100)
|
||||
if retrieve < 1:
|
||||
return
|
||||
|
||||
data, state, limit = await strategy(retrieve, state, limit)
|
||||
|
||||
# Terminate loop on next iteration; there's no data left after this
|
||||
if len(data) < 1000:
|
||||
limit = 0
|
||||
|
||||
for e in data:
|
||||
yield Subscription(data=e, state=self._state)
|
||||
|
||||
|
||||
class Entitlement:
|
||||
"""Represents an entitlement from user or guild which has been granted access to a premium offering.
|
||||
@ -190,17 +335,12 @@ class Entitlement:
|
||||
|
||||
Raises
|
||||
-------
|
||||
MissingApplicationID
|
||||
The application ID could not be found.
|
||||
NotFound
|
||||
The entitlement could not be found.
|
||||
HTTPException
|
||||
Consuming the entitlement failed.
|
||||
"""
|
||||
|
||||
if self.application_id is None:
|
||||
raise MissingApplicationID
|
||||
|
||||
await self._state.http.consume_entitlement(self.application_id, self.id)
|
||||
|
||||
async def delete(self) -> None:
|
||||
@ -210,15 +350,10 @@ class Entitlement:
|
||||
|
||||
Raises
|
||||
-------
|
||||
MissingApplicationID
|
||||
The application ID could not be found.
|
||||
NotFound
|
||||
The entitlement could not be found.
|
||||
HTTPException
|
||||
Deleting the entitlement failed.
|
||||
"""
|
||||
|
||||
if self.application_id is None:
|
||||
raise MissingApplicationID
|
||||
|
||||
await self._state.http.delete_entitlement(self.application_id, self.id)
|
||||
|
@ -79,6 +79,8 @@ from .automod import AutoModRule, AutoModAction
|
||||
from .audit_logs import AuditLogEntry
|
||||
from ._types import ClientT
|
||||
from .soundboard import SoundboardSound
|
||||
from .subscription import Subscription
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .abc import PrivateChannel
|
||||
@ -1736,6 +1738,18 @@ class ConnectionState(Generic[ClientT]):
|
||||
if poll:
|
||||
self.dispatch('poll_vote_remove', user, poll.get_answer(raw.answer_id))
|
||||
|
||||
def parse_subscription_create(self, data: gw.SubscriptionCreateEvent) -> None:
|
||||
subscription = Subscription(data=data, state=self)
|
||||
self.dispatch('subscription_create', subscription)
|
||||
|
||||
def parse_subscription_update(self, data: gw.SubscriptionUpdateEvent) -> None:
|
||||
subscription = Subscription(data=data, state=self)
|
||||
self.dispatch('subscription_update', subscription)
|
||||
|
||||
def parse_subscription_delete(self, data: gw.SubscriptionDeleteEvent) -> None:
|
||||
subscription = Subscription(data=data, state=self)
|
||||
self.dispatch('subscription_delete', subscription)
|
||||
|
||||
def _get_reaction_user(self, channel: MessageableChannel, user_id: int) -> Optional[Union[User, Member]]:
|
||||
if isinstance(channel, (TextChannel, Thread, VoiceChannel)):
|
||||
return channel.guild.get_member(user_id)
|
||||
|
103
discord/subscription.py
Normal file
103
discord/subscription.py
Normal file
@ -0,0 +1,103 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from typing import List, Optional, TYPE_CHECKING
|
||||
|
||||
from . import utils
|
||||
from .mixins import Hashable
|
||||
from .enums import try_enum, SubscriptionStatus
|
||||
|
||||
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 Discord subscription.
|
||||
|
||||
.. versionadded:: 2.5
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
id: :class:`int`
|
||||
The subscription's ID.
|
||||
user_id: :class:`int`
|
||||
The ID of the user that is subscribed.
|
||||
sku_ids: List[:class:`int`]
|
||||
The IDs of the SKUs that the user subscribed to.
|
||||
entitlement_ids: List[:class:`int`]
|
||||
The IDs of the entitlements granted for this subscription.
|
||||
current_period_start: :class:`datetime.datetime`
|
||||
When the current billing period started.
|
||||
current_period_end: :class:`datetime.datetime`
|
||||
When the current billing period ends.
|
||||
status: :class:`SubscriptionStatus`
|
||||
The status of the subscription.
|
||||
canceled_at: Optional[:class:`datetime.datetime`]
|
||||
When the subscription was canceled.
|
||||
This is only available for subscriptions with a :attr:`status` of :attr:`SubscriptionStatus.inactive`.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
'_state',
|
||||
'id',
|
||||
'user_id',
|
||||
'sku_ids',
|
||||
'entitlement_ids',
|
||||
'current_period_start',
|
||||
'current_period_end',
|
||||
'status',
|
||||
'canceled_at',
|
||||
)
|
||||
|
||||
def __init__(self, *, state: ConnectionState, data: SubscriptionPayload):
|
||||
self._state = 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.current_period_start: datetime.datetime = utils.parse_time(data['current_period_start'])
|
||||
self.current_period_end: datetime.datetime = utils.parse_time(data['current_period_end'])
|
||||
self.status: SubscriptionStatus = try_enum(SubscriptionStatus, data['status'])
|
||||
self.canceled_at: Optional[datetime.datetime] = utils.parse_time(data['canceled_at'])
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<Subscription id={self.id} user_id={self.user_id} status={self.status!r}>'
|
||||
|
||||
@property
|
||||
def created_at(self) -> datetime.datetime:
|
||||
""":class:`datetime.datetime`: Returns the subscription's creation time in UTC."""
|
||||
return utils.snowflake_time(self.id)
|
||||
|
||||
@property
|
||||
def user(self) -> Optional[User]:
|
||||
"""Optional[:class:`User`]: The user that is subscribed."""
|
||||
return self._state.get_user(self.user_id)
|
@ -46,6 +46,7 @@ from .threads import Thread, ThreadMember
|
||||
from .scheduled_event import GuildScheduledEvent
|
||||
from .audit_log import AuditLogEntry
|
||||
from .soundboard import SoundboardSound
|
||||
from .subscription import Subscription
|
||||
|
||||
|
||||
class SessionStartLimit(TypedDict):
|
||||
@ -372,3 +373,6 @@ class PollVoteActionEvent(TypedDict):
|
||||
message_id: Snowflake
|
||||
guild_id: NotRequired[Snowflake]
|
||||
answer_id: int
|
||||
|
||||
|
||||
SubscriptionCreateEvent = SubscriptionUpdateEvent = SubscriptionDeleteEvent = Subscription
|
||||
|
42
discord/types/subscription.py
Normal file
42
discord/types/subscription.py
Normal file
@ -0,0 +1,42 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Literal, Optional, TypedDict
|
||||
|
||||
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]
|
||||
current_period_start: str
|
||||
current_period_end: str
|
||||
status: SubscriptionStatus
|
||||
canceled_at: Optional[str]
|
58
docs/api.rst
58
docs/api.rst
@ -1356,6 +1356,37 @@ Stages
|
||||
:param after: The stage instance after the update.
|
||||
:type after: :class:`StageInstance`
|
||||
|
||||
|
||||
Subscriptions
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
.. function:: on_subscription_create(subscription)
|
||||
|
||||
Called when a subscription is created.
|
||||
|
||||
.. versionadded:: 2.5
|
||||
|
||||
:param subscription: The subscription that was created.
|
||||
:type subscription: :class:`Subscription`
|
||||
|
||||
.. function:: on_subscription_update(subscription)
|
||||
|
||||
Called when a subscription is updated.
|
||||
|
||||
.. versionadded:: 2.5
|
||||
|
||||
:param subscription: The subscription that was updated.
|
||||
:type subscription: :class:`Subscription`
|
||||
|
||||
.. function:: on_subscription_delete(subscription)
|
||||
|
||||
Called when a subscription is deleted.
|
||||
|
||||
.. versionadded:: 2.5
|
||||
|
||||
:param subscription: The subscription that was deleted.
|
||||
:type subscription: :class:`Subscription`
|
||||
|
||||
Threads
|
||||
~~~~~~~~
|
||||
|
||||
@ -3754,6 +3785,25 @@ of :class:`enum.Enum`.
|
||||
The standard animation.
|
||||
|
||||
|
||||
.. class:: SubscriptionStatus
|
||||
|
||||
Represents the status of an subscription.
|
||||
|
||||
.. versionadded:: 2.5
|
||||
|
||||
.. attribute:: active
|
||||
|
||||
The subscription is active.
|
||||
|
||||
.. attribute:: ending
|
||||
|
||||
The subscription is active but will not renew.
|
||||
|
||||
.. attribute:: inactive
|
||||
|
||||
The subscription is inactive and not being charged.
|
||||
|
||||
|
||||
.. _discord-api-audit-logs:
|
||||
|
||||
Audit Log Data
|
||||
@ -5151,6 +5201,14 @@ Entitlement
|
||||
.. autoclass:: Entitlement()
|
||||
:members:
|
||||
|
||||
Subscription
|
||||
~~~~~~~~~~~~
|
||||
|
||||
.. attributetable:: Subscription
|
||||
|
||||
.. autoclass:: Subscription()
|
||||
:members:
|
||||
|
||||
RawMessageDeleteEvent
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user