You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

632 lines
22 KiB

"""
The MIT License (MIT)
Copyright (c) 2021-present Dolfies
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 TYPE_CHECKING, Any, List, Optional
from .enums import EntitlementType, GiftStyle, PremiumType, try_enum
from .flags import GiftFlags
from .mixins import Hashable
from .payments import EntitlementPayment
from .promotions import Promotion
from .store import SKU, StoreListing, SubscriptionPlan
from .subscriptions import Subscription, SubscriptionTrial
from .utils import _get_as_snowflake, parse_time, utcnow
if TYPE_CHECKING:
from datetime import datetime
from .abc import Snowflake
from .guild import Guild
from .state import ConnectionState
from .types.entitlements import (
Entitlement as EntitlementPayload,
Gift as GiftPayload,
GiftBatch as GiftBatchPayload,
)
from .user import User
__all__ = (
'Entitlement',
'Gift',
'GiftBatch',
)
class Entitlement(Hashable):
"""Represents a Discord entitlement.
.. container:: operations
.. describe:: x == y
Checks if two entitlements are equal.
.. describe:: x != y
Checks if two entitlements are not equal.
.. describe:: hash(x)
Returns the entitlement's hash.
.. describe:: bool(x)
Checks if the entitlement is active.
.. versionadded:: 2.0
Attributes
----------
id: :class:`int`
The ID of the entitlement.
type: :class:`EntitlementType`
The type of entitlement.
user_id: :class:`int`
The ID of the user the entitlement is for.
sku_id: :class:`int`
The ID of the SKU the entitlement grants.
application_id: :class:`int`
The ID of the application that owns the SKU the entitlement grants.
promotion_id: Optional[:class:`int`]
The ID of the promotion the entitlement is from.
parent_id: Optional[:class:`int`]
The ID of the entitlement's parent.
guild_id: Optional[:class:`int`]
The ID of the guild the entitlement is for.
branches: List[:class:`int`]
The IDs of the branches the entitlement grants.
gifter_id: Optional[:class:`int`]
The ID of the user that gifted the entitlement.
gift_style: Optional[:class:`GiftStyle`]
The style of the gift attached to this entitlement.
gift_batch_id: Optional[:class:`int`]
The ID of the batch the gift attached to this entitlement is from.
deleted: :class:`bool`
Whether the entitlement is deleted.
consumed: :class:`bool`
Whether the entitlement is consumed.
starts_at: Optional[:class:`datetime.datetime`]
When the entitlement period starts.
ends_at: Optional[:class:`datetime.datetime`]
When the entitlement period ends.
subscription_id: Optional[:class:`int`]
The ID of the subscription the entitlement is from.
subscription_plan: Optional[:class:`SubscriptionPlan`]
The subscription plan the entitlement is for.
.. note::
This is a partial object without price information.
sku: Optional[:class:`SKU`]
The SKU the entitlement grants.
payment: Optional[:class:`EntitlementPayment`]
The payment made for the entitlement.
Not available in some contexts.
"""
__slots__ = (
'id',
'type',
'user_id',
'sku_id',
'application_id',
'promotion_id',
'parent_id',
'guild_id',
'branches',
'gifter_id',
'gift_style',
'gift_batch_id',
'_gift_flags',
'deleted',
'consumed',
'starts_at',
'ends_at',
'subscription_id',
'subscription_plan',
'sku',
'payment',
'_state',
)
def __init__(self, *, data: EntitlementPayload, state: ConnectionState):
self._state = state
self._update(data)
def _update(self, data: EntitlementPayload):
state = self._state
self.id: int = int(data['id'])
self.type: EntitlementType = try_enum(EntitlementType, data['type'])
self.user_id: int = int(data.get('user_id') or state.self_id) # type: ignore
self.sku_id: int = int(data['sku_id'])
self.application_id: int = int(data['application_id'])
self.promotion_id: Optional[int] = _get_as_snowflake(data, 'promotion_id')
self.parent_id: Optional[int] = _get_as_snowflake(data, 'parent_id')
self.guild_id: Optional[int] = _get_as_snowflake(data, 'guild_id')
self.branches: List[int] = [int(branch) for branch in data.get('branches', [])]
self.gifter_id: Optional[int] = _get_as_snowflake(data, 'gifter_user_id')
self.gift_style: Optional[GiftStyle] = try_enum(GiftStyle, data.get('gift_style'))
self.gift_batch_id: Optional[int] = _get_as_snowflake(data, 'gift_code_batch_id')
self._gift_flags: int = data.get('gift_code_flags', 0)
self.deleted: bool = data.get('deleted', False)
self.consumed: bool = data.get('consumed', False)
self.starts_at: Optional[datetime] = parse_time(data.get('starts_at'))
self.ends_at: Optional[datetime] = parse_time(data.get('ends_at'))
self.subscription_id: Optional[int] = _get_as_snowflake(data, 'subscription_id')
self.subscription_plan: Optional[SubscriptionPlan] = (
SubscriptionPlan(data=data['subscription_plan'], state=state) if 'subscription_plan' in data else None
)
self.sku: Optional[SKU] = SKU(data=data['sku'], state=state) if 'sku' in data else None
self.payment: Optional[EntitlementPayment] = (
EntitlementPayment(data=data['payment'], entitlement=self) if 'payment' in data else None
)
def __repr__(self) -> str:
return f'<Entitlement id={self.id} type={self.type!r} sku_id={self.sku_id} application_id={self.application_id}>'
def __bool__(self) -> bool:
return self.is_active()
@property
def guild(self) -> Optional[Guild]:
""":class:`Guild`: Returns the guild the entitlement is for, if accessible."""
return self._state._get_guild(self.guild_id)
@property
def premium_type(self) -> Optional[PremiumType]:
"""Optional[:class:`PremiumType`]: The premium type this entitlement grants, if it is for a premium subscription."""
return PremiumType.from_sku_id(self.sku_id)
@property
def gift_flags(self) -> GiftFlags:
""":class:`GiftFlags`: Returns the flags for the gift this entitlement is attached to."""
return GiftFlags._from_value(self._gift_flags)
def is_giftable(self) -> bool:
""":class:`bool`: Whether the entitlement is giftable."""
return self.type == EntitlementType.user_gift and not self.gifter_id
def is_active(self) -> bool:
""":class:`bool`: Whether the entitlement is active and offering perks."""
# This is a copy of the logic used in the client
if self.is_giftable() or self.deleted:
return False # Giftable entitlements have not yet been gifted therefore are not active
if self.starts_at and self.starts_at > utcnow():
return False # Entitlement has not started yet
if self.ends_at and self.ends_at < utcnow():
return False # Entitlement has ended
if self.type == EntitlementType.premium_subscription:
# Premium subscription entitlements are only active
# if the SKU is offered for free to premium subscribers
# and the user is a premium subscriber
sku = self.sku
if sku and not sku.premium:
return False
if self._state.user and not self._state.user.premium_type == PremiumType.nitro:
return False
return True
async def subscription(self) -> Optional[Subscription]:
"""|coro|
Retrieves the subscription this entitlement is attached to, if applicable.
Raises
------
NotFound
You cannot access this subscription.
HTTPException
Fetching the subscription failed.
Returns
-------
Optional[:class:`Subscription`]
The retrieved subscription, if applicable.
"""
if not self.subscription_id:
return
data = await self._state.http.get_subscription(self.subscription_id)
return Subscription(data=data, state=self._state)
async def consume(self) -> None:
"""|coro|
Consumes the entitlement. This marks a given user entitlement as expended,
and removes the entitlement from the user's active entitlements.
This should be called after the user has received the relevant item,
and only works on entitlements for SKUs of type :attr:`SKUType.consumable`.
Raises
------
Forbidden
You do not have permissions to access this application.
HTTPException
Consuming the entitlement failed.
"""
await self._state.http.consume_app_entitlement(self.application_id, self.id)
async def delete(self) -> None:
"""|coro|
Deletes the entitlement. This removes the entitlement from the user's
entitlements, and is irreversible.
This is only useable on entitlements of type :attr:`EntitlementType.test_mode_purchase`.
Raises
------
Forbidden
You do not have permissions to access this application.
HTTPException
Deleting the entitlement failed.
"""
await self._state.http.delete_app_entitlement(self.application_id, self.id)
class Gift:
"""Represents a Discord gift.
.. container:: operations
.. describe:: x == y
Checks if two gifts are equal.
.. describe:: x != y
Checks if two gifts are not equal.
.. describe:: hash(x)
Returns the gift's hash.
.. versionadded:: 2.0
Attributes
----------
code: :class:`str`
The gift's code.
expires_at: Optional[:class:`datetime.datetime`]
When the gift expires.
application_id: Optional[:class:`int`]
The ID of the application that owns the SKU the gift is for.
Not available in all contexts.
batch_id: Optional[:class:`int`]
The ID of the batch the gift is from.
sku_id: :class:`int`
The ID of the SKU the gift is for.
entitlement_branches: List[:class:`int`]
A list of entitlements the gift is for.
gift_style: Optional[:class:`GiftStyle`]
The style of the gift.
max_uses: :class:`int`
The maximum number of times the gift can be used.
uses: :class:`int`
The number of times the gift has been used.
redeemed: :class:`bool`
Whether the user has redeemed the gift.
revoked: :class:`bool`
Whether the gift has been revoked.
guild_id: Optional[:class:`int`]
The ID of the guild the gift was redeemed in.
Not available in all contexts.
channel_id: Optional[:class:`int`]
The ID of the channel the gift was redeemed in.
Not available in all contexts.
store_listing: Optional[:class:`StoreListing`]
The store listing for the SKU the gift is for.
Not available in all contexts.
promotion: Optional[:class:`Promotion`]
The promotion the gift is a part of, if any.
subscription_trial: Optional[:class:`SubscriptionTrial`]
The subscription trial the gift is a part of, if any.
subscription_plan_id: Optional[:class:`int`]
The ID of the subscription plan the gift is for, if any.
subscription_plan: Optional[:class:`SubscriptionPlan`]
The subscription plan the gift is for, if any.
user: Optional[:class:`User`]
The user who created the gift, if applicable.
"""
__slots__ = (
'code',
'expires_at',
'application_id',
'batch_id',
'sku_id',
'entitlement_branches',
'gift_style',
'_flags',
'max_uses',
'uses',
'redeemed',
'revoked',
'guild_id',
'channel_id',
'store_listing',
'promotion',
'subscription_trial',
'subscription_plan_id',
'subscription_plan',
'user',
'_state',
)
def __init__(self, *, data: GiftPayload, state: ConnectionState) -> None:
self._state = state
self._update(data)
def _update(self, data: GiftPayload) -> None:
state = self._state
self.code: str = data['code']
self.expires_at: Optional[datetime] = parse_time(data.get('expires_at'))
self.application_id: Optional[int] = _get_as_snowflake(data, 'application_id')
self.batch_id: Optional[int] = _get_as_snowflake(data, 'batch_id')
self.subscription_plan_id: Optional[int] = _get_as_snowflake(data, 'subscription_plan_id')
self.sku_id: int = int(data['sku_id'])
self.entitlement_branches: List[int] = [int(x) for x in data.get('entitlement_branches', [])]
self.gift_style: Optional[GiftStyle] = try_enum(GiftStyle, data['gift_style']) if data.get('gift_style') else None # type: ignore
self._flags: int = data.get('flags', 0)
self.max_uses: int = data.get('max_uses', 0)
self.uses: int = data.get('uses', 0)
self.redeemed: bool = data.get('redeemed', False)
self.revoked: bool = data.get('revoked', False)
self.guild_id: Optional[int] = _get_as_snowflake(data, 'guild_id')
self.channel_id: Optional[int] = _get_as_snowflake(data, 'channel_id')
self.store_listing: Optional[StoreListing] = (
StoreListing(data=data['store_listing'], state=state) if 'store_listing' in data else None
)
self.promotion: Optional[Promotion] = Promotion(data=data['promotion'], state=state) if 'promotion' in data else None
self.subscription_trial: Optional[SubscriptionTrial] = (
SubscriptionTrial(data['subscription_trial']) if 'subscription_trial' in data else None
)
self.subscription_plan: Optional[SubscriptionPlan] = (
SubscriptionPlan(data=data['subscription_plan'], state=state) if 'subscription_plan' in data else None
)
self.user: Optional[User] = self._state.create_user(data['user']) if 'user' in data else None
def __repr__(self) -> str:
return f'<Gift code={self.code!r} sku_id={self.sku_id} uses={self.uses} max_uses={self.max_uses} redeemed={self.redeemed}>'
def __eq__(self, other: Any) -> bool:
return isinstance(other, Gift) and other.code == self.code
def __ne__(self, other: Any) -> bool:
if isinstance(other, Gift):
return other.code != self.code
return True
def __hash__(self) -> int:
return hash(self.code)
@property
def id(self) -> str:
""":class:`str`: Returns the code portion of the gift."""
return self.code
@property
def url(self) -> str:
""":class:`str`: Returns the gift's URL."""
return f'https://discord.gift/{self.code}'
@property
def remaining_uses(self) -> int:
""":class:`int`: Returns the number of remaining uses for the gift."""
return self.max_uses - self.uses
@property
def flags(self) -> GiftFlags:
""":class:`GiftFlags`: Returns the gift's flags."""
return GiftFlags._from_value(self._flags)
@property
def premium_type(self) -> Optional[PremiumType]:
"""Optional[:class:`PremiumType`]: The premium type this gift grants, if it is for a premium subscription."""
return PremiumType.from_sku_id(self.sku_id) if self.is_subscription() else None
def is_claimed(self) -> bool:
""":class:`bool`: Checks if the gift has been used up."""
return self.uses >= self.max_uses if self.max_uses else False
def is_expired(self) -> bool:
""":class:`bool`: Checks if the gift has expired."""
return self.expires_at < utcnow() if self.expires_at else False
def is_subscription(self) -> bool:
""":class:`bool`: Checks if the gift is for a subscription."""
return self.subscription_plan_id is not None
def is_premium_subscription(self) -> bool:
""":class:`bool`: Checks if the gift is for a premium subscription."""
return self.is_subscription() and self.application_id == self._state.premium_subscriptions_application.id
async def redeem(
self,
payment_source: Optional[Snowflake] = None,
*,
channel: Optional[Snowflake] = None,
gateway_checkout_context: Optional[str] = None,
) -> Entitlement:
"""|coro|
Redeems the gift.
Parameters
----------
payment_source: Optional[:class:`PaymentSource`]
The payment source to use for the redemption.
Only required if the gift's :attr:`flags` have :attr:`GiftFlags.payment_source_required` set to ``True``.
channel: Optional[Union[:class:`TextChannel`, :class:`VoiceChannel`, :class:`StageChannel`, :class:`Thread`, :class:`DMChannel`, :class:`GroupChannel`]]
The channel to redeem the gift in. This is usually the channel the gift was sent in.
While this is optional, it is recommended to pass this in.
gateway_checkout_context: Optional[:class:`str`]
The current checkout context.
Raises
------
HTTPException
The gift failed to redeem.
Returns
-------
:class:`Entitlement`
The entitlement that was created from redeeming the gift.
"""
data = await self._state.http.redeem_gift(
self.code,
payment_source.id if payment_source else None,
channel.id if channel else None,
gateway_checkout_context,
)
return Entitlement(data=data, state=self._state)
async def delete(self) -> None:
"""|coro|
Revokes the gift.
This is only possible for gifts the current account has created.
Raises
------
NotFound
The owned gift was not found.
HTTPException
The gift failed to delete.
"""
await self._state.http.delete_gift(self.code)
class GiftBatch(Hashable):
"""Represents a batch of gifts for an SKU.
.. container:: operations
.. describe:: x == y
Checks if two gift batches are equal.
.. describe:: x != y
Checks if two gift batches are not equal.
.. describe:: hash(x)
Returns the gift batch's hash.
.. describe:: str(x)
Returns the gift batch's description.
.. versionadded:: 2.0
Attributes
-----------
id: :class:`int`
The ID of the gift batch.
application_id: :class:`int`
The ID of the application the gift batch is for.
sku_id: :class:`int`
The ID of the SKU the gift batch is for.
amount: :class:`int`
The amount of gifts in the batch.
description: :class:`str`
The description of the gift batch.
entitlement_branches: List[:class:`int`]
The entitlement branches the gift batch is for.
entitlement_starts_at: Optional[:class:`datetime.datetime`]
When the entitlement is valid from.
entitlement_ends_at: Optional[:class:`datetime.datetime`]
When the entitlement is valid until.
"""
__slots__ = (
'id',
'application_id',
'sku_id',
'amount',
'description',
'entitlement_branches',
'entitlement_starts_at',
'entitlement_ends_at',
'_state',
)
def __init__(self, *, data: GiftBatchPayload, state: ConnectionState, application_id: int) -> None:
self._state: ConnectionState = state
self.id: int = int(data['id'])
self.application_id = application_id
self.sku_id: int = int(data['sku_id'])
self.amount: int = data['amount']
self.description: str = data.get('description', '')
self.entitlement_branches: List[int] = [int(branch) for branch in data.get('entitlement_branches', [])]
self.entitlement_starts_at: Optional[datetime] = parse_time(data.get('entitlement_starts_at'))
self.entitlement_ends_at: Optional[datetime] = parse_time(data.get('entitlement_ends_at'))
def __repr__(self) -> str:
return f'<GiftBatch id={self.id} sku_id={self.sku_id} amount={self.amount} description={self.description!r}>'
def __str__(self) -> str:
return self.description
def is_valid(self) -> bool:
""":class:`bool`: Checks if the gift batch is valid."""
if self.entitlement_starts_at and self.entitlement_starts_at > utcnow():
return False
if self.entitlement_ends_at and self.entitlement_ends_at < utcnow():
return False
return True
async def download(self) -> bytes:
"""|coro|
Returns the gifts in the gift batch in CSV format.
Raises
-------
Forbidden
You do not have permissions to download the batch.
HTTPException
Downloading the batch failed.
Returns
-------
:class:`bytes`
The report content.
"""
return await self._state.http.get_gift_batch_csv(self.application_id, self.id)