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.

2343 lines
91 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 datetime import datetime, timedelta
from typing import TYPE_CHECKING, Any, Collection, Dict, List, Mapping, Optional, Sequence, Tuple, Union
from .asset import Asset, AssetMixin
from .enums import (
ContentRatingAgency,
ESRBContentDescriptor,
ESRBRating,
GiftStyle,
Locale,
OperatingSystem,
PEGIContentDescriptor,
PEGIRating,
PremiumType,
SKUAccessLevel,
SKUFeature,
SKUGenre,
SKUProductLine,
SKUType,
SubscriptionInterval,
SubscriptionPlanPurchaseType,
try_enum,
)
from .flags import SKUFlags
from .mixins import Hashable
from .utils import (
MISSING,
_get_as_snowflake,
_get_extension_for_mime_type,
_parse_localizations,
get,
parse_date,
parse_time,
utcnow,
)
if TYPE_CHECKING:
from datetime import date
from typing_extensions import Self
from .abc import Snowflake
from .application import Application, PartialApplication
from .entitlements import Entitlement, Gift, GiftBatch
from .guild import Guild
from .library import LibraryApplication
from .state import ConnectionState
from .types.application import StoreAsset as StoreAssetPayload
from .types.entitlements import Gift as GiftPayload
from .types.snowflake import Snowflake as SnowflakeType
from .types.store import (
SKU as SKUPayload,
CarouselItem as CarouselItemPayload,
ContentRating as ContentRatingPayload,
PremiumPrice as PremiumPricePayload,
SKUPrice as SKUPricePayload,
StoreListing as StoreListingPayload,
StoreNote as StoreNotePayload,
SystemRequirements as SystemRequirementsPayload,
)
from .types.subscriptions import (
PartialSubscriptionPlan as PartialSubscriptionPlanPayload,
SubscriptionPlan as SubscriptionPlanPayload,
SubscriptionPrice as SubscriptionPricePayload,
SubscriptionPrices as SubscriptionPricesPayload,
)
from .user import User
__all__ = (
'StoreAsset',
'StoreNote',
'SystemRequirements',
'StoreListing',
'SKUPrice',
'ContentRating',
'SKU',
'SubscriptionPlanPrices',
'SubscriptionPlan',
)
THE_GAME_AWARDS_WINNERS = (500428425362931713, 451550535720501248, 471376328319303681, 466696214818193408)
class StoreAsset(AssetMixin, Hashable):
"""Represents an application store asset.
.. container:: operations
.. describe:: x == y
Checks if two assets are equal.
.. describe:: x != y
Checks if two assets are not equal.
.. describe:: hash(x)
Returns the asset's hash.
.. versionadded:: 2.0
Attributes
-----------
parent: Union[:class:`StoreListing`, :class:`Application`]
The store listing or application that this asset belongs to.
id: Union[:class:`int`, :class:`str`]
The asset's ID or YouTube video ID.
size: :class:`int`
The asset's size in bytes, or 0 if it's a YouTube video.
height: :class:`int`
The asset's height in pixels, or 0 if it's a YouTube video.
width: :class:`int`
The asset's width in pixels, or 0 if it's a YouTube video.
mime_type: :class:`str`
The asset's mime type, or "video/youtube" if it is a YouTube video.
"""
__slots__ = ('_state', 'parent', 'id', 'size', 'height', 'width', 'mime_type')
def __init__(self, *, data: StoreAssetPayload, state: ConnectionState, parent: Union[StoreListing, Application]) -> None:
self._state: ConnectionState = state
self.parent = parent
self.size: int = data['size']
self.height: int = data['height']
self.width: int = data['width']
self.mime_type: str = data['mime_type']
self.id: SnowflakeType
try:
self.id = int(data['id'])
except ValueError:
self.id = data['id']
@classmethod
def _from_id(
cls, *, id: SnowflakeType, mime_type: str = '', state: ConnectionState, parent: Union[StoreListing, Application]
) -> StoreAsset:
data: StoreAssetPayload = {'id': id, 'size': 0, 'height': 0, 'width': 0, 'mime_type': mime_type}
return cls(data=data, state=state, parent=parent)
@classmethod
def _from_carousel_item(
cls, *, data: CarouselItemPayload, state: ConnectionState, store_listing: StoreListing
) -> StoreAsset:
asset_id = _get_as_snowflake(data, 'asset_id')
if asset_id:
return get(store_listing.assets, id=asset_id) or StoreAsset._from_id(
id=asset_id, state=state, parent=store_listing
)
else:
# One or the other must be present
return cls._from_id(id=data['youtube_video_id'], mime_type='video/youtube', state=state, parent=store_listing) # type: ignore
def __repr__(self) -> str:
return f'<ApplicationAsset id={self.id} height={self.height} width={self.width}>'
@property
def application_id(self) -> int:
""":class:`int`: Returns the application ID that this asset belongs to."""
parent = self.parent
return parent.sku.application_id if hasattr(parent, 'sku') else parent.id # type: ignore # Type checker doesn't understand
@property
def animated(self) -> bool:
""":class:`bool`: Indicates if the store asset is animated."""
return self.mime_type in {'video/youtube', 'image/gif', 'video/mp4'}
@property
def url(self) -> str:
""":class:`str`: Returns the URL of the store asset."""
if self.is_youtube_video():
return f'https://youtube.com/watch?v={self.id}'
return (
f'{Asset.BASE}/app-assets/{self.application_id}/store/{self.id}.{_get_extension_for_mime_type(self.mime_type)}'
)
def is_youtube_video(self) -> bool:
""":class:`bool`: Indicates if the asset is a YouTube video."""
return self.mime_type == 'video/youtube'
def to_carousel_item(self) -> dict:
if self.is_youtube_video():
return {'youtube_video_id': self.id}
return {'asset_id': self.id}
async def read(self) -> bytes:
"""|coro|
Retrieves the content of this asset as a :class:`bytes` object.
Raises
------
ValueError
The asset is a YouTube video.
HTTPException
Downloading the asset failed.
NotFound
The asset was deleted.
Returns
-------
:class:`bytes`
The content of the asset.
"""
if self.is_youtube_video():
raise ValueError('StoreAsset is not a real asset')
return await super().read()
async def delete(self) -> None:
"""|coro|
Deletes the asset.
Raises
------
ValueError
The asset is a YouTube video.
Forbidden
You are not allowed to delete this asset.
HTTPException
Deleting the asset failed.
"""
if self.is_youtube_video():
raise ValueError('StoreAsset is not a real asset')
await self._state.http.delete_store_asset(self.application_id, self.id)
class StoreNote:
"""Represents a note for a store listing.
.. container:: operations
.. describe:: str(x)
Returns the note's content.
.. versionadded:: 2.0
Attributes
-----------
user: Optional[:class:`User`]
The user who wrote the note.
content: :class:`str`
The note content.
"""
__slots__ = ('user', 'content')
def __init__(self, *, data: StoreNotePayload, state: ConnectionState) -> None:
self.user: Optional[User] = state.create_user(data['user']) if data.get('user') else None # type: ignore
self.content: str = data['content']
def __repr__(self) -> str:
return f'<StoreNote user={self.user!r} content={self.content!r}>'
def __str__(self) -> str:
return self.content
class SystemRequirements:
"""Represents system requirements.
.. versionadded:: 2.0
Attributes
-----------
os: :class:`OperatingSystem`
The operating system these requirements apply to.
minimum_os_version: :class:`str`
The minimum operating system version required.
recommended_os_version: :class:`str`
The recommended operating system version.
minimum_cpu: :class:`str`
The minimum CPU specifications required.
recommended_cpu: :class:`str`
The recommended CPU specifications.
minimum_gpu: :class:`str`
The minimum GPU specifications required.
recommended_gpu: :class:`str`
The recommended GPU specifications.
minimum_ram: :class:`int`
The minimum RAM size in megabytes.
recommended_ram: :class:`int`
The recommended RAM size in megabytes.
minimum_disk: :class:`int`
The minimum free storage space in megabytes.
recommended_disk: :class:`int`
The recommended free storage space in megabytes.
minimum_sound_card: Optional[:class:`str`]
The minimum sound card specifications required, if any.
recommended_sound_card: Optional[:class:`str`]
The recommended sound card specifications, if any.
minimum_directx: Optional[:class:`str`]
The minimum DirectX version required, if any.
recommended_directx: Optional[:class:`str`]
The recommended DirectX version, if any.
minimum_network: Optional[:class:`str`]
The minimum network specifications required, if any.
recommended_network: Optional[:class:`str`]
The recommended network specifications, if any.
minimum_notes: Optional[:class:`str`]
Any extra notes on minimum requirements.
recommended_notes: Optional[:class:`str`]
Any extra notes on recommended requirements.
"""
# I hate this class so much
if TYPE_CHECKING:
os: OperatingSystem
minimum_ram: Optional[int]
recommended_ram: Optional[int]
minimum_disk: Optional[int]
recommended_disk: Optional[int]
minimum_os_version: Optional[str]
minimum_os_version_localizations: Dict[Locale, str]
recommended_os_version: Optional[str]
recommended_os_version_localizations: Dict[Locale, str]
minimum_cpu: Optional[str]
minimum_cpu_localizations: Dict[Locale, str]
recommended_cpu: Optional[str]
recommended_cpu_localizations: Dict[Locale, str]
minimum_gpu: Optional[str]
minimum_gpu_localizations: Dict[Locale, str]
recommended_gpu: Optional[str]
recommended_gpu_localizations: Dict[Locale, str]
minimum_sound_card: Optional[str]
minimum_sound_card_localizations: Dict[Locale, str]
recommended_sound_card: Optional[str]
recommended_sound_card_localizations: Dict[Locale, str]
minimum_directx: Optional[str]
minimum_directx_localizations: Dict[Locale, str]
recommended_directx: Optional[str]
recommended_directx_localizations: Dict[Locale, str]
minimum_network: Optional[str]
minimum_network_localizations: Dict[Locale, str]
recommended_network: Optional[str]
recommended_network_localizations: Dict[Locale, str]
minimum_notes: Optional[str]
minimum_notes_localizations: Dict[Locale, str]
recommended_notes: Optional[str]
recommended_notes_localizations: Dict[Locale, str]
__slots__ = (
'os',
'minimum_ram',
'recommended_ram',
'minimum_disk',
'recommended_disk',
'minimum_os_version',
'minimum_os_version_localizations',
'recommended_os_version',
'recommended_os_version_localizations',
'minimum_cpu',
'minimum_cpu_localizations',
'recommended_cpu',
'recommended_cpu_localizations',
'minimum_gpu',
'minimum_gpu_localizations',
'recommended_gpu',
'recommended_gpu_localizations',
'minimum_sound_card',
'minimum_sound_card_localizations',
'recommended_sound_card',
'recommended_sound_card_localizations',
'minimum_directx',
'minimum_directx_localizations',
'recommended_directx',
'recommended_directx_localizations',
'minimum_network',
'minimum_network_localizations',
'recommended_network',
'recommended_network_localizations',
'minimum_notes',
'minimum_notes_localizations',
'recommended_notes',
'recommended_notes_localizations',
)
def __init__(
self,
os: OperatingSystem,
*,
minimum_ram: Optional[int] = None,
recommended_ram: Optional[int] = None,
minimum_disk: Optional[int] = None,
recommended_disk: Optional[int] = None,
minimum_os_version: Optional[str] = None,
minimum_os_version_localizations: Optional[Dict[Locale, str]] = None,
recommended_os_version: Optional[str] = None,
recommended_os_version_localizations: Optional[Dict[Locale, str]] = None,
minimum_cpu: Optional[str] = None,
minimum_cpu_localizations: Optional[Dict[Locale, str]] = None,
recommended_cpu: Optional[str] = None,
recommended_cpu_localizations: Optional[Dict[Locale, str]] = None,
minimum_gpu: Optional[str] = None,
minimum_gpu_localizations: Optional[Dict[Locale, str]] = None,
recommended_gpu: Optional[str] = None,
recommended_gpu_localizations: Optional[Dict[Locale, str]] = None,
minimum_sound_card: Optional[str] = None,
minimum_sound_card_localizations: Optional[Dict[Locale, str]] = None,
recommended_sound_card: Optional[str] = None,
recommended_sound_card_localizations: Optional[Dict[Locale, str]] = None,
minimum_directx: Optional[str] = None,
minimum_directx_localizations: Optional[Dict[Locale, str]] = None,
recommended_directx: Optional[str] = None,
recommended_directx_localizations: Optional[Dict[Locale, str]] = None,
minimum_network: Optional[str] = None,
minimum_network_localizations: Optional[Dict[Locale, str]] = None,
recommended_network: Optional[str] = None,
recommended_network_localizations: Optional[Dict[Locale, str]] = None,
minimum_notes: Optional[str] = None,
minimum_notes_localizations: Optional[Dict[Locale, str]] = None,
recommended_notes: Optional[str] = None,
recommended_notes_localizations: Optional[Dict[Locale, str]] = None,
) -> None:
self.os = os
self.minimum_ram = minimum_ram
self.recommended_ram = recommended_ram
self.minimum_disk = minimum_disk
self.recommended_disk = recommended_disk
self.minimum_os_version = minimum_os_version
self.minimum_os_version_localizations = minimum_os_version_localizations or {}
self.recommended_os_version = recommended_os_version
self.recommended_os_version_localizations = recommended_os_version_localizations or {}
self.minimum_cpu = minimum_cpu
self.minimum_cpu_localizations = minimum_cpu_localizations or {}
self.recommended_cpu = recommended_cpu
self.recommended_cpu_localizations = recommended_cpu_localizations or {}
self.minimum_gpu = minimum_gpu
self.minimum_gpu_localizations = minimum_gpu_localizations or {}
self.recommended_gpu = recommended_gpu
self.recommended_gpu_localizations = recommended_gpu_localizations or {}
self.minimum_sound_card = minimum_sound_card
self.minimum_sound_card_localizations = minimum_sound_card_localizations or {}
self.recommended_sound_card = recommended_sound_card
self.recommended_sound_card_localizations = recommended_sound_card_localizations or {}
self.minimum_directx = minimum_directx
self.minimum_directx_localizations = minimum_directx_localizations or {}
self.recommended_directx = recommended_directx
self.recommended_directx_localizations = recommended_directx_localizations or {}
self.minimum_network = minimum_network
self.minimum_network_localizations = minimum_network_localizations or {}
self.recommended_network = recommended_network
self.recommended_network_localizations = recommended_network_localizations or {}
self.minimum_notes = minimum_notes
self.minimum_notes_localizations = minimum_notes_localizations or {}
self.recommended_notes = recommended_notes
self.recommended_notes_localizations = recommended_notes_localizations or {}
@classmethod
def from_dict(cls, os: OperatingSystem, data: SystemRequirementsPayload) -> Self:
minimum = data.get('minimum', {})
recommended = data.get('recommended', {})
minimum_os_version, minimum_os_version_localizations = _parse_localizations(minimum, 'operating_system_version')
recommended_os_version, recommended_os_version_localizations = _parse_localizations(
recommended, 'operating_system_version'
)
minimum_cpu, minimum_cpu_localizations = _parse_localizations(minimum, 'cpu')
recommended_cpu, recommended_cpu_localizations = _parse_localizations(recommended, 'cpu')
minimum_gpu, minimum_gpu_localizations = _parse_localizations(minimum, 'gpu')
recommended_gpu, recommended_gpu_localizations = _parse_localizations(recommended, 'gpu')
minimum_sound_card, minimum_sound_card_localizations = _parse_localizations(minimum, 'sound_card')
recommended_sound_card, recommended_sound_card_localizations = _parse_localizations(recommended, 'sound_card')
minimum_directx, minimum_directx_localizations = _parse_localizations(minimum, 'directx')
recommended_directx, recommended_directx_localizations = _parse_localizations(recommended, 'directx')
minimum_network, minimum_network_localizations = _parse_localizations(minimum, 'network')
recommended_network, recommended_network_localizations = _parse_localizations(recommended, 'network')
minimum_notes, minimum_notes_localizations = _parse_localizations(minimum, 'notes')
recommended_notes, recommended_notes_localizations = _parse_localizations(recommended, 'notes')
return cls(
os,
minimum_ram=minimum.get('ram'),
recommended_ram=recommended.get('ram'),
minimum_disk=minimum.get('disk'),
recommended_disk=recommended.get('disk'),
minimum_os_version=minimum_os_version,
minimum_os_version_localizations=minimum_os_version_localizations,
recommended_os_version=recommended_os_version,
recommended_os_version_localizations=recommended_os_version_localizations,
minimum_cpu=minimum_cpu,
minimum_cpu_localizations=minimum_cpu_localizations,
recommended_cpu=recommended_cpu,
recommended_cpu_localizations=recommended_cpu_localizations,
minimum_gpu=minimum_gpu,
minimum_gpu_localizations=minimum_gpu_localizations,
recommended_gpu=recommended_gpu,
recommended_gpu_localizations=recommended_gpu_localizations,
minimum_sound_card=minimum_sound_card,
minimum_sound_card_localizations=minimum_sound_card_localizations,
recommended_sound_card=recommended_sound_card,
recommended_sound_card_localizations=recommended_sound_card_localizations,
minimum_directx=minimum_directx,
minimum_directx_localizations=minimum_directx_localizations,
recommended_directx=recommended_directx,
recommended_directx_localizations=recommended_directx_localizations,
minimum_network=minimum_network,
minimum_network_localizations=minimum_network_localizations,
recommended_network=recommended_network,
recommended_network_localizations=recommended_network_localizations,
minimum_notes=minimum_notes,
minimum_notes_localizations=minimum_notes_localizations,
recommended_notes=recommended_notes,
recommended_notes_localizations=recommended_notes_localizations,
)
def __repr__(self) -> str:
return f'<SystemRequirements os={self.os!r}>'
def to_dict(self) -> dict:
minimum = {}
recommended = {}
for key in self.__slots__:
if key.endswith('_localizations'):
continue
value = getattr(self, key)
localizations = getattr(self, f'{key}_localizations', None)
if value or localizations:
data = (
value
if localizations is None
else {'default': value, 'localizations': {str(k): v for k, v in localizations.items()}}
)
if key.startswith('minimum_'):
minimum[key[8:]] = data
elif key.startswith('recommended_'):
recommended[key[12:]] = data
return {'minimum': minimum, 'recommended': recommended}
class StoreListing(Hashable):
"""Represents a store listing.
.. container:: operations
.. describe:: x == y
Checks if two listings are equal.
.. describe:: x != y
Checks if two listings are not equal.
.. describe:: hash(x)
Returns the listing's hash.
.. describe:: str(x)
Returns the listing's summary.
.. versionadded:: 2.0
Attributes
-----------
id: :class:`int`
The listing's ID.
summary: Optional[:class:`str`]
The listing's summary.
summary_localizations: Dict[:class:`Locale`, :class:`str`]
The listing's summary localized to different languages.
description: Optional[:class:`str`]
The listing's description.
description_localizations: Dict[:class:`Locale`, :class:`str`]
The listing's description localized to different languages.
tagline: Optional[:class:`str`]
The listing's tagline.
tagline_localizations: Dict[:class:`Locale`, :class:`str`]
The listing's tagline localized to different languages.
flavor: Optional[:class:`str`]
The listing's flavor text.
sku: :class:`SKU`
The SKU attached to this listing.
child_skus: List[:class:`SKU`]
The child SKUs attached to this listing.
alternative_skus: List[:class:`SKU`]
Alternative SKUs to the one attached to this listing.
guild: Optional[:class:`Guild`]
The guild tied to this listing, if any.
published: :class:`bool`
Whether the listing is published and publicly visible.
published_at: Optional[:class:`datetime.datetime`]
When the listing was published, if available.
.. note::
This data is not available for all listings.
.. versionadded:: 2.1
unpublished_at: Optional[:class:`datetime.datetime`]
When the listing was last unpublished, if available.
If this is a future date, the listing will be unpublished at that time.
.. versionadded:: 2.1
staff_note: Optional[:class:`StoreNote`]
The staff note attached to this listing.
assets: List[:class:`StoreAsset`]
A list of assets used in this listing.
carousel_items: List[:class:`StoreAsset`]
A list of assets and YouTube videos displayed in the carousel.
preview_video: Optional[:class:`StoreAsset`]
The preview video of the store listing.
header_background: Optional[:class:`StoreAsset`]
The header background image.
hero_background: Optional[:class:`StoreAsset`]
The hero background image.
box_art: Optional[:class:`StoreAsset`]
The box art of the product.
thumbnail: Optional[:class:`StoreAsset`]
The listing's thumbnail.
header_logo_light: Optional[:class:`StoreAsset`]
The header logo image for light backgrounds.
header_logo_dark: Optional[:class:`StoreAsset`]
The header logo image for dark backgrounds.
"""
__slots__ = (
'_state',
'id',
'summary',
'summary_localizations',
'description',
'description_localizations',
'tagline',
'tagline_localizations',
'flavor',
'sku',
'child_skus',
'alternative_skus',
'entitlement_branch_id',
'guild',
'published',
'published_at',
'unpublished_at',
'staff_note',
'assets',
'carousel_items',
'preview_video',
'header_background',
'hero_background',
'hero_video',
'box_art',
'thumbnail',
'header_logo_light',
'header_logo_dark',
)
if TYPE_CHECKING:
summary: Optional[str]
summary_localizations: Dict[Locale, str]
description: Optional[str]
description_localizations: Dict[Locale, str]
tagline: Optional[str]
tagline_localizations: Dict[Locale, str]
def __init__(
self, *, data: StoreListingPayload, state: ConnectionState, application: Optional[PartialApplication] = None
) -> None:
self._state = state
self._update(data, application=application)
def __str__(self) -> str:
return self.summary or ''
def __repr__(self) -> str:
return f'<StoreListing id={self.id} summary={self.summary!r} sku={self.sku!r}>'
def _update(self, data: StoreListingPayload, application: Optional[PartialApplication] = None) -> None:
state = self._state
self.summary, self.summary_localizations = _parse_localizations(data, 'summary')
self.description, self.description_localizations = _parse_localizations(data, 'description')
self.tagline, self.tagline_localizations = _parse_localizations(data, 'tagline')
self.id: int = int(data['id'])
self.flavor: Optional[str] = data.get('flavor_text')
self.sku: SKU = SKU(data=data['sku'], state=state, application=application)
self.child_skus: List[SKU] = [SKU(data=sku, state=state) for sku in data.get('child_skus', [])]
self.alternative_skus: List[SKU] = [SKU(data=sku, state=state) for sku in data.get('alternative_skus', [])]
self.entitlement_branch_id: Optional[int] = _get_as_snowflake(data, 'entitlement_branch_id')
self.guild: Optional[Guild] = state.create_guild(data['guild']) if 'guild' in data else None # type: ignore
self.published: bool = data.get('published', True)
self.published_at: Optional[datetime] = parse_time(data['published_at']) if 'published_at' in data else None
self.unpublished_at: Optional[datetime] = parse_time(data['unpublished_at']) if 'unpublished_at' in data else None
self.staff_note: Optional[StoreNote] = (
StoreNote(data=data['staff_notes'], state=state) if 'staff_notes' in data else None
)
self.assets: List[StoreAsset] = [
StoreAsset(data=asset, state=state, parent=self) for asset in data.get('assets', [])
]
self.carousel_items: List[StoreAsset] = [
StoreAsset._from_carousel_item(data=asset, state=state, store_listing=self)
for asset in data.get('carousel_items', [])
]
self.preview_video: Optional[StoreAsset] = (
StoreAsset(data=data['preview_video'], state=state, parent=self) if 'preview_video' in data else None
)
self.header_background: Optional[StoreAsset] = (
StoreAsset(data=data['header_background'], state=state, parent=self) if 'header_background' in data else None
)
self.hero_background: Optional[StoreAsset] = (
StoreAsset(data=data['hero_background'], state=state, parent=self) if 'hero_background' in data else None
)
self.hero_video: Optional[StoreAsset] = (
StoreAsset(data=data['hero_video'], state=state, parent=self) if 'hero_video' in data else None
)
self.box_art: Optional[StoreAsset] = (
StoreAsset(data=data['box_art'], state=state, parent=self) if 'box_art' in data else None
)
self.thumbnail: Optional[StoreAsset] = (
StoreAsset(data=data['thumbnail'], state=state, parent=self) if 'thumbnail' in data else None
)
self.header_logo_light: Optional[StoreAsset] = (
StoreAsset(data=data['header_logo_light_theme'], state=state, parent=self)
if 'header_logo_light_theme' in data
else None
)
self.header_logo_dark: Optional[StoreAsset] = (
StoreAsset(data=data['header_logo_dark_theme'], state=state, parent=self)
if 'header_logo_dark_theme' in data
else None
)
async def edit(
self,
*,
summary: Optional[str] = MISSING,
summary_localizations: Mapping[Locale, str] = MISSING,
description: Optional[str] = MISSING,
description_localizations: Mapping[Locale, str] = MISSING,
tagline: Optional[str] = MISSING,
tagline_localizations: Mapping[Locale, str] = MISSING,
child_skus: Sequence[Snowflake] = MISSING,
guild: Optional[Snowflake] = MISSING,
published: bool = MISSING,
carousel_items: Sequence[Union[StoreAsset, str]] = MISSING,
preview_video: Optional[Snowflake] = MISSING,
header_background: Optional[Snowflake] = MISSING,
hero_background: Optional[Snowflake] = MISSING,
hero_video: Optional[Snowflake] = MISSING,
box_art: Optional[Snowflake] = MISSING,
thumbnail: Optional[Snowflake] = MISSING,
header_logo_light: Optional[Snowflake] = MISSING,
header_logo_dark: Optional[Snowflake] = MISSING,
):
"""|coro|
Edits the store listing.
All parameters are optional.
Parameters
----------
summary: Optional[:class:`str`]
The summary of the store listing.
summary_localizations: Mapping[:class:`Locale`, :class:`str`]
The summary of the store listing localized to different languages.
description: Optional[:class:`str`]
The description of the store listing.
description_localizations: Mapping[:class:`Locale`, :class:`str`]
The description of the store listing localized to different languages.
tagline: Optional[:class:`str`]
The tagline of the store listing.
tagline_localizations: Mapping[:class:`Locale`, :class:`str`]
The tagline of the store listing localized to different languages.
child_skus: List[:class:`SKU`]
The child SKUs of the store listing.
guild: Optional[:class:`Guild`]
The guild that the store listing is for.
published: :class:`bool`
Whether the store listing is published.
carousel_items: List[Union[:class:`StoreAsset`, :class:`str`]]
A list of carousel items to add to the store listing. These can be store assets or YouTube video IDs.
preview_video: Optional[:class:`StoreAsset`]
The preview video of the store listing.
header_background: Optional[:class:`StoreAsset`]
The header background of the store listing.
hero_background: Optional[:class:`StoreAsset`]
The hero background of the store listing.
hero_video: Optional[:class:`StoreAsset`]
The hero video of the store listing.
box_art: Optional[:class:`StoreAsset`]
The box art of the store listing.
thumbnail: Optional[:class:`StoreAsset`]
The thumbnail of the store listing.
header_logo_light: Optional[:class:`StoreAsset`]
The header logo image for light backgrounds.
header_logo_dark: Optional[:class:`StoreAsset`]
The header logo image for dark backgrounds.
Raises
------
Forbidden
You do not have permissions to edit the store listing.
HTTPException
Editing the store listing failed.
"""
payload = {}
if summary is not MISSING or summary_localizations is not MISSING:
localizations = (
(summary_localizations or {}) if summary_localizations is not MISSING else self.summary_localizations
)
payload['name'] = {
'default': (summary if summary is not MISSING else self.summary) or '',
'localizations': {str(k): v for k, v in localizations.items()},
}
if description is not MISSING or description_localizations is not MISSING:
localizations = (
(description_localizations or {})
if description_localizations is not MISSING
else self.description_localizations
)
payload['description'] = {
'default': (description if description is not MISSING else self.description) or '',
'localizations': {str(k): v for k, v in localizations.items()},
}
if tagline is not MISSING or tagline_localizations is not MISSING:
localizations = (
(tagline_localizations or {}) if tagline_localizations is not MISSING else self.tagline_localizations
)
payload['tagline'] = {
'default': (tagline if tagline is not MISSING else self.tagline) or '',
'localizations': {str(k): v for k, v in localizations.items()},
}
if child_skus is not MISSING:
payload['child_sku_ids'] = [sku.id for sku in child_skus] if child_skus else []
if guild is not MISSING:
payload['guild_id'] = guild.id if guild else None
if published is not MISSING:
payload['published'] = published
if carousel_items is not MISSING:
payload['carousel_items'] = (
[
item.to_carousel_item() if isinstance(item, StoreAsset) else {'youtube_video_id': item}
for item in carousel_items
]
if carousel_items
else []
)
if preview_video is not MISSING:
payload['preview_video_asset_id'] = preview_video.id if preview_video else None
if header_background is not MISSING:
payload['header_background_asset_id'] = header_background.id if header_background else None
if hero_background is not MISSING:
payload['hero_background_asset_id'] = hero_background.id if hero_background else None
if hero_video is not MISSING:
payload['hero_video_asset_id'] = hero_video.id if hero_video else None
if box_art is not MISSING:
payload['box_art_asset_id'] = box_art.id if box_art else None
if thumbnail is not MISSING:
payload['thumbnail_asset_id'] = thumbnail.id if thumbnail else None
if header_logo_light is not MISSING:
payload['header_logo_light_theme_asset_id'] = header_logo_light.id if header_logo_light else None
if header_logo_dark is not MISSING:
payload['header_logo_dark_theme_asset_id'] = header_logo_dark.id if header_logo_dark else None
data = await self._state.http.edit_store_listing(self.id, payload)
self._update(data, application=self.sku.application)
@property
def url(self) -> str:
""":class:`str`: Returns the URL of the store listing. This is the URL of the primary SKU."""
return self.sku.url
class SKUPrice:
"""Represents a SKU's price.
.. container:: operations
.. describe:: bool(x)
Checks if a SKU costs anything.
.. describe:: int(x)
Returns the price of the SKU.
.. versionadded:: 2.0
Attributes
-----------
currency: :class:`str`
The currency of the price.
amount: :class:`int`
The price of the SKU.
sale_amount: Optional[:class:`int`]
The price of the SKU with discounts applied, if any.
sale_percentage: :class:`int`
The percentage of the price discounted, if any.
exponent: :class:`int`
The offset of the currency's decimal point.
For example, if the price is 1000 and the exponent is 2, the price is $10.00.
.. versionadded:: 2.1
premium: Dict[:class:`PremiumType`, :class:`SKUPrice`]
Special SKU prices for premium (Nitro) users.
.. versionadded:: 2.1
"""
__slots__ = ('currency', 'amount', 'sale_amount', 'sale_percentage', 'exponent', 'premium')
def __init__(self, data: Union[SKUPricePayload, SubscriptionPricePayload]) -> None:
self.currency: str = data.get('currency', 'usd')
self.amount: int = data.get('amount', 0)
self.sale_amount: Optional[int] = data.get('sale_amount')
self.sale_percentage: int = data.get('sale_percentage', 0)
self.exponent: int = data.get('exponent', data.get('currency_exponent', 0))
self.premium: Dict[PremiumType, SKUPrice] = {
try_enum(PremiumType, premium_type): SKUPrice.from_premium(self, premium_data)
for premium_type, premium_data in data.get('premium', {}).items()
}
@classmethod
def from_private(cls, data: SKUPayload) -> SKUPrice:
payload: SKUPricePayload = {
'currency': 'usd',
'currency_exponent': 2,
'amount': data.get('price_tier') or 0,
'sale_amount': data.get('sale_price_tier'),
}
if payload['sale_amount'] is not None:
payload['sale_percentage'] = int((1 - (payload['sale_amount'] / payload['amount'])) * 100)
return cls(payload)
@classmethod
def from_premium(cls, parent: SKUPrice, data: PremiumPricePayload) -> SKUPrice:
payload: SKUPricePayload = {
'currency': parent.currency,
'currency_exponent': parent.exponent,
'amount': parent.amount,
'sale_amount': data.get('amount'),
'sale_percentage': data.get('percentage'),
}
return cls(payload)
def __repr__(self) -> str:
return f'<SKUPrice amount={self.amount} currency={self.currency!r}>'
def __bool__(self) -> bool:
return self.amount > 0
def __int__(self) -> int:
return self.amount
def is_discounted(self) -> bool:
""":class:`bool`: Checks whether the SKU is discounted."""
return self.sale_percentage > 0
def is_free(self) -> bool:
""":class:`bool`: Checks whether the SKU is free.
.. versionchanged:: 2.1
This now also checks the :attr:`sale_amount` to see if the SKU is free with discounts applied.
"""
return self.sale_amount == 0 or self.amount == 0
@property
def discounts(self) -> int:
""":class:`int`: Returns the amount of discounts applied to the SKU price."""
return self.amount - (self.sale_amount or self.amount)
class ContentRating:
"""Represents a SKU's content rating.
.. versionadded:: 2.0
Attributes
-----------
agency: :class:`ContentRatingAgency`
The agency that rated the content.
rating: Union[:class:`ESRBRating`, :class:`PEGIRating`]
The rating of the content.
descriptors: Union[List[:class:`ESRBContentDescriptor`], List[:class:`PEGIContentDescriptor`]
Extra descriptors for the content rating.
"""
_AGENCY_MAP = {
ContentRatingAgency.esrb: (ESRBRating, ESRBContentDescriptor),
ContentRatingAgency.pegi: (PEGIRating, PEGIContentDescriptor),
}
__slots__ = ('agency', 'rating', 'descriptors')
def __init__(
self,
*,
agency: ContentRatingAgency,
rating: Union[ESRBRating, PEGIRating],
descriptors: Union[Collection[ESRBContentDescriptor], Collection[PEGIContentDescriptor]],
) -> None:
self.agency = agency
ratingcls, descriptorcls = self._AGENCY_MAP[agency]
self.rating: Union[ESRBRating, PEGIRating] = try_enum(ratingcls, int(rating))
self.descriptors: Union[List[ESRBContentDescriptor], List[PEGIContentDescriptor]] = [
try_enum(descriptorcls, int(descriptor)) for descriptor in descriptors
]
@classmethod
def from_dict(cls, data: ContentRatingPayload, agency: int) -> ContentRating:
return cls(
agency=try_enum(ContentRatingAgency, agency),
rating=data.get('rating', 1), # type: ignore # Faked
descriptors=data.get('descriptors', []), # type: ignore # Faked
)
@classmethod
def from_dicts(cls, datas: Optional[dict]) -> List[ContentRating]:
if not datas:
return []
return [cls.from_dict(data, int(agency)) for agency, data in datas.items()]
def __repr__(self) -> str:
return f'<ContentRating agency={self.agency!r} rating={self.rating}>'
def to_dict(self) -> dict:
return {'rating': int(self.rating), 'descriptors': [int(descriptor) for descriptor in self.descriptors]}
class SKU(Hashable):
"""Represents a store SKU.
.. container:: operations
.. describe:: x == y
Checks if two SKUs are equal.
.. describe:: x != y
Checks if two SKUs are not equal.
.. describe:: hash(x)
Returns the SKU's hash.
.. describe:: str(x)
Returns the SKU's name.
.. versionadded:: 2.0
Attributes
-----------
id: :class:`int`
The SKU's ID.
name: :class:`str`
The name of the SKU.
name_localizations: Dict[:class:`Locale`, :class:`str`]
The name of the SKU localized to different languages.
summary: Optional[:class:`str`]
The SKU's summary, if any.
summary_localizations: Dict[:class:`Locale`, :class:`str`]
The summary of the SKU localized to different languages.
legal_notice: Optional[:class:`str`]
The SKU's legal notice, if any.
legal_notice_localizations: Dict[:class:`Locale`, :class:`str`]
The legal notice of the SKU localized to different languages.
type: :class:`SKUType`
The type of the SKU.
product_line: Optional[:class:`SKUProductLine`]
The product line of the SKU, if any.
.. versionadded:: 2.1
slug: :class:`str`
The URL slug of the SKU.
dependent_sku_id: Optional[:class:`int`]
The ID of the SKU that this SKU is dependent on, if any.
application_id: :class:`int`
The ID of the application that owns this SKU.
application: Optional[:class:`PartialApplication`]
The application that owns this SKU, if available.
price_tier: Optional[:class:`int`]
The price tier of the SKU. This is the base price in USD.
Not available for public SKUs.
price_overrides: Dict[:class:`str`, :class:`int`]
Price overrides for specific currencies. These override the base price tier.
Not available for public SKUs.
sale_price_tier: Optional[:class:`int`]
The sale price tier of the SKU. This is the base sale price in USD.
Not available for public SKUs.
sale_price_overrides: Dict[:class:`str`, :class:`int`]
Sale price overrides for specific currencies. These override the base sale price tier.
price: :class:`SKUPrice`
The price of the SKU.
access_level: :class:`SKUAccessLevel`
The access level of the SKU.
features: List[:class:`SKUFeature`]
A list of features that this SKU has.
locales: List[:class:`Locale`]
The locales that this SKU is available in.
genres: List[:class:`SKUGenre`]
The genres that apply to this SKU.
available_regions: Optional[List[:class:`str`]]
The regions that this SKU is available in.
If this is ``None``, then the SKU is available everywhere.
content_ratings: List[:class:`ContentRating`]
The content ratings of the SKU, if any.
For public SKUs, only the rating of your region is returned.
system_requirements: List[:class:`SystemRequirements`]
The system requirements of the SKU by operating system, if any.
release_date: Optional[:class:`datetime.date`]
The date that the SKU will released, if any.
preorder_release_date: Optional[:class:`datetime.date`]
The approximate date that the SKU will released for pre-order, if any.
preorder_released_at: Optional[:class:`datetime.datetime`]
The date that the SKU was released for pre-order, if any.
external_purchase_url: Optional[:class:`str`]
An external URL to purchase the SKU at, if applicable.
premium: :class:`bool`
Whether this SKU is provided for free to premium users.
restricted: :class:`bool`
Whether this SKU is restricted.
exclusive: :class:`bool`
Whether this SKU is exclusive to Discord.
deleted: :class:`bool`
Whether this SKU has been soft-deleted.
.. versionadded:: 2.1
show_age_gate: :class:`bool`
Whether the client should prompt the user to verify their age.
bundled_skus: List[:class:`SKU`]
A list of SKUs bundled with this SKU.
These are SKUs that the user will be entitled to after purchasing this parent SKU.
manifest_label_ids: List[:class:`int`]
A list of manifest label IDs that this SKU is associated with.
"""
__slots__ = (
'id',
'name',
'name_localizations',
'summary',
'summary_localizations',
'legal_notice',
'legal_notice_localizations',
'type',
'product_line',
'slug',
'price_tier',
'price_overrides',
'sale_price_tier',
'sale_price_overrides',
'price',
'dependent_sku_id',
'application_id',
'application',
'access_level',
'features',
'locales',
'genres',
'available_regions',
'content_ratings',
'system_requirements',
'release_date',
'preorder_release_date',
'preorder_released_at',
'external_purchase_url',
'premium',
'restricted',
'exclusive',
'deleted',
'show_age_gate',
'bundled_skus',
'manifests',
'manifest_label_ids',
'_flags',
'_state',
)
if TYPE_CHECKING:
name: str
name_localizations: Dict[Locale, str]
summary: Optional[str]
summary_localizations: Dict[Locale, str]
legal_notice: Optional[str]
legal_notice_localizations: Dict[Locale, str]
def __init__(
self, *, data: SKUPayload, state: ConnectionState, application: Optional[PartialApplication] = None
) -> None:
self._state = state
self.application = application
self._update(data)
def __str__(self) -> str:
return self.name
def __repr__(self) -> str:
return f'<SKU id={self.id} name={self.name!r} type={self.type!r}>'
def _update(self, data: SKUPayload) -> None:
from .application import PartialApplication
state = self._state
self.name, self.name_localizations = _parse_localizations(data, 'name')
self.summary, self.summary_localizations = _parse_localizations(data, 'summary')
self.legal_notice, self.legal_notice_localizations = _parse_localizations(data, 'legal_notice')
self.id: int = int(data['id'])
self.type: SKUType = try_enum(SKUType, data['type'])
self.product_line: Optional[SKUProductLine] = (
try_enum(SKUProductLine, data['product_line']) if data.get('product_line') else None
)
self.slug: str = data['slug']
self.dependent_sku_id: Optional[int] = _get_as_snowflake(data, 'dependent_sku_id')
self.application_id: int = int(data['application_id'])
self.application: Optional[PartialApplication] = (
PartialApplication(data=data['application'], state=state)
if 'application' in data
else (
state.premium_subscriptions_application
if self.application_id == state.premium_subscriptions_application.id
else self.application
)
)
self._flags: int = data.get('flags', 0)
# This hurts me, but we have two cases here:
# - The SKU is public and we get our local price/sale in the `price` field (an object in its entirety)
# - The SKU is private and we get the `price`/`sale` (overrides) and `price_tier`/`sale_price_tier` fields
# In the above case, we construct a fake price object from the fields given
# Unfortunately, in both cases, the `price` field may just be missing if there is no price set
self.price_tier: Optional[int] = data.get('price_tier')
self.price_overrides: Dict[str, int] = data.get('price') or {} # type: ignore
self.sale_price_tier: Optional[int] = data.get('sale_price_tier')
self.sale_price_overrides: Dict[str, int] = data.get('sale_price') or {}
if self.price_overrides and any(x in self.price_overrides for x in ('amount', 'currency')):
self.price: SKUPrice = SKUPrice(data['price']) # type: ignore
self.price_overrides = {}
else:
self.price = SKUPrice.from_private(data)
self.access_level: SKUAccessLevel = try_enum(SKUAccessLevel, data.get('access_type', 1))
self.features: List[SKUFeature] = [try_enum(SKUFeature, feature) for feature in data.get('features', [])]
self.locales: List[Locale] = [try_enum(Locale, locale) for locale in data.get('locales', ['en-US'])]
self.genres: List[SKUGenre] = [try_enum(SKUGenre, genre) for genre in data.get('genres', [])]
self.available_regions: Optional[List[str]] = data.get('available_regions')
self.content_ratings: List[ContentRating] = (
[ContentRating.from_dict(data['content_rating'], data['content_rating_agency'])] # type: ignore
if 'content_rating' in data and 'content_rating_agency' in data
else ContentRating.from_dicts(data.get('content_ratings'))
)
self.system_requirements: List[SystemRequirements] = [
SystemRequirements.from_dict(try_enum(OperatingSystem, int(os)), reqs)
for os, reqs in data.get('system_requirements', {}).items()
]
self.release_date: Optional[date] = parse_date(data.get('release_date'))
self.preorder_release_date: Optional[date] = parse_date(data.get('preorder_approximate_release_date'))
self.preorder_released_at: Optional[datetime] = parse_time(data.get('preorder_release_at'))
self.external_purchase_url: Optional[str] = data.get('external_purchase_url')
self.premium: bool = data.get('premium', False)
self.restricted: bool = data.get('restricted', False)
self.exclusive: bool = data.get('exclusive', False)
self.deleted: bool = data.get('deleted', False)
self.show_age_gate: bool = data.get('show_age_gate', False)
self.bundled_skus: List[SKU] = [
SKU(data=sku, state=state, application=self.application) for sku in data.get('bundled_skus', [])
]
self.manifest_label_ids: List[int] = [int(label) for label in data.get('manifest_labels') or []]
def is_free(self) -> bool:
""":class:`bool`: Checks if the SKU is free."""
return self.price.is_free() and not self.premium
def is_paid(self) -> bool:
""":class:`bool`: Checks if the SKU requires payment."""
return not self.price.is_free() and not self.premium
def is_preorder(self) -> bool:
""":class:`bool`: Checks if this SKU is a preorder."""
return self.preorder_release_date is not None or self.preorder_released_at is not None
def is_released(self) -> bool:
""":class:`bool`: Checks if the SKU is released."""
return self.release_date is not None and self.release_date <= utcnow()
def is_giftable(self) -> bool:
""":class:`bool`: Checks if this SKU is giftable."""
return (
self.type == SKUType.durable_primary
and self.flags.available
and not self.external_purchase_url
and self.is_paid()
)
def is_premium_perk(self) -> bool:
""":class:`bool`: Checks if the SKU is a perk for premium users."""
return self.premium and (self.flags.premium_and_distribution or self.flags.premium_purchase)
def is_premium_subscription(self) -> bool:
""":class:`bool`: Checks if the SKU is a premium subscription (e.g. Nitro or Server Boosts)."""
return self.application_id == self._state.premium_subscriptions_application.id
def is_game_awards_winner(self) -> bool:
""":class:`bool`: Checks if the SKU is a winner of The Game Awards."""
return self.id in THE_GAME_AWARDS_WINNERS
@property
def url(self) -> str:
""":class:`str`: Returns the URL of the SKU."""
return f'https://discord.com/store/skus/{self.id}/{self.slug}'
@property
def flags(self) -> SKUFlags:
""":class:`SKUFlags`: Returns the SKU's flags."""
return SKUFlags._from_value(self._flags)
@property
def supported_operating_systems(self) -> List[OperatingSystem]:
"""List[:class:`OperatingSystem`]: A list of supported operating systems."""
return [reqs.os for reqs in self.system_requirements] or [OperatingSystem.windows]
async def edit(
self,
name: str = MISSING,
name_localizations: Mapping[Locale, str] = MISSING,
legal_notice: Optional[str] = MISSING,
legal_notice_localizations: Mapping[Locale, str] = MISSING,
price_tier: Optional[int] = MISSING,
price_overrides: Mapping[str, int] = MISSING,
sale_price_tier: Optional[int] = MISSING,
sale_price_overrides: Mapping[str, int] = MISSING,
dependent_sku: Optional[Snowflake] = MISSING,
flags: SKUFlags = MISSING,
access_level: SKUAccessLevel = MISSING,
features: Collection[SKUFeature] = MISSING,
locales: Collection[Locale] = MISSING,
genres: Collection[SKUGenre] = MISSING,
content_ratings: Collection[ContentRating] = MISSING,
system_requirements: Collection[SystemRequirements] = MISSING,
release_date: Optional[date] = MISSING,
bundled_skus: Sequence[Snowflake] = MISSING,
manifest_labels: Sequence[Snowflake] = MISSING,
) -> None:
"""|coro|
Edits the SKU.
All parameters are optional.
Parameters
-----------
name: :class:`str`
The SKU's name.
name_localizations: Mapping[:class:`Locale`, :class:`str`]
The SKU's name localized to other languages.
legal_notice: Optional[:class:`str`]
The SKU's legal notice.
legal_notice_localizations: Mapping[:class:`Locale`, :class:`str`]
The SKU's legal notice localized to other languages.
price_tier: Optional[:class:`int`]
The price tier of the SKU.
This is the base price in USD that other currencies will be calculated from.
price_overrides: Mapping[:class:`str`, :class:`int`]
A mapping of currency to price. These prices override the base price tier.
sale_price_tier: Optional[:class:`int`]
The sale price tier of the SKU.
This is the base sale price in USD that other currencies will be calculated from.
sale_price_overrides: Mapping[:class:`str`, :class:`int`]
A mapping of currency to sale price. These prices override the base sale price tier.
dependent_sku: Optional[:class:`SKU`]
The ID of the SKU that this SKU is dependent on.
flags: :class:`SKUFlags`
The SKU's flags.
access_level: :class:`SKUAccessLevel`
The access level of the SKU.
features: List[:class:`SKUFeature`]
A list of features of the SKU.
locales: List[:class:`Locale`]
A list of locales supported by the SKU.
genres: List[:class:`SKUGenre`]
A list of genres of the SKU.
content_ratings: List[:class:`ContentRating`]
A list of content ratings of the SKU.
system_requirements: List[:class:`SystemRequirements`]
A list of system requirements of the SKU.
release_date: Optional[:class:`datetime.date`]
The release date of the SKU.
bundled_skus: List[:class:`SKU`]
A list SKUs that are bundled with this SKU.
manifest_labels: List[:class:`ManifestLabel`]
A list of manifest labels for the SKU.
Raises
------
Forbidden
You do not have access to edit the SKU.
HTTPException
Editing the SKU failed.
"""
payload = {}
if name is not MISSING or name_localizations is not MISSING:
payload['name'] = {
'default': name or self.name,
'localizations': {
str(k): v
for k, v in (
(name_localizations or {}) if name_localizations is not MISSING else self.name_localizations
).items()
},
}
if legal_notice or legal_notice_localizations:
payload['legal_notice'] = {
'default': legal_notice,
'localizations': {
str(k): v
for k, v in (
(legal_notice_localizations or {})
if legal_notice_localizations is not MISSING
else self.legal_notice_localizations
).items()
},
}
if price_tier is not MISSING:
payload['price_tier'] = price_tier
if price_overrides is not MISSING:
payload['price'] = {str(k): v for k, v in price_overrides.items()}
if sale_price_tier is not MISSING:
payload['sale_price_tier'] = sale_price_tier
if sale_price_overrides is not MISSING:
payload['sale_price'] = {str(k): v for k, v in (sale_price_overrides or {}).items()}
if dependent_sku is not MISSING:
payload['dependent_sku_id'] = dependent_sku.id if dependent_sku else None
if flags is not MISSING:
payload['flags'] = flags.value if flags else 0
if access_level is not MISSING:
payload['access_level'] = int(access_level)
if locales is not MISSING:
payload['locales'] = [str(l) for l in locales] if locales else []
if features is not MISSING:
payload['features'] = [int(f) for f in features] if features else []
if genres is not MISSING:
payload['genres'] = [int(g) for g in genres] if genres else []
if content_ratings is not MISSING:
payload['content_ratings'] = (
{content_rating.agency: content_rating.to_dict() for content_rating in content_ratings}
if content_ratings
else {}
)
if system_requirements is not MISSING:
payload['system_requirements'] = (
{system_requirement.os: system_requirement.to_dict() for system_requirement in system_requirements}
if system_requirements
else {}
)
if release_date is not MISSING:
payload['release_date'] = release_date.isoformat() if release_date else None
if bundled_skus is not MISSING:
payload['bundled_skus'] = [s.id for s in bundled_skus] if bundled_skus else []
if manifest_labels is not MISSING:
payload['manifest_labels'] = [m.id for m in manifest_labels] if manifest_labels else []
data = await self._state.http.edit_sku(self.id, **payload)
self._update(data)
async def subscription_plans(
self,
*,
country_code: str = MISSING,
payment_source: Snowflake = MISSING,
with_unpublished: bool = False,
) -> List[SubscriptionPlan]:
r"""|coro|
Returns a list of :class:`SubscriptionPlan`\s for this SKU.
.. versionadded:: 2.0
Parameters
----------
country_code: :class:`str`
The country code to retrieve the subscription plan prices for.
Defaults to the country code of the current user.
payment_source: :class:`PaymentSource`
The specific payment source to retrieve the subscription plan prices for.
Defaults to all payment sources of the current user.
with_unpublished: :class:`bool`
Whether to include unpublished subscription plans.
If ``True``, then you require access to the application.
Raises
------
HTTPException
Retrieving the subscription plans failed.
Returns
-------
List[:class:`.SubscriptionPlan`]
The subscription plans for this SKU.
"""
state = self._state
data = await state.http.get_store_listing_subscription_plans(
self.id,
country_code=country_code if country_code is not MISSING else None,
payment_source_id=payment_source.id if payment_source is not MISSING else None,
include_unpublished=with_unpublished,
)
return [SubscriptionPlan(state=state, data=d) for d in data]
async def store_listings(self, localize: bool = True) -> List[StoreListing]:
r"""|coro|
Returns a list of :class:`StoreListing`\s for this SKU.
Parameters
-----------
localize: :class:`bool`
Whether to localize the store listings to the current user's locale.
If ``False`` then all localizations are returned.
Raises
------
Forbidden
You do not have access to fetch store listings.
HTTPException
Retrieving the store listings failed.
Returns
-------
List[:class:`StoreListing`]
The store listings for this SKU.
"""
data = await self._state.http.get_sku_store_listings(self.id, localize=localize)
return [StoreListing(data=listing, state=self._state, application=self.application) for listing in data]
async def create_store_listing(
self,
*,
summary: str,
summary_localizations: Optional[Mapping[Locale, str]] = None,
description: str,
description_localizations: Optional[Mapping[Locale, str]] = None,
tagline: Optional[str] = None,
tagline_localizations: Optional[Mapping[Locale, str]] = None,
child_skus: Optional[Collection[Snowflake]] = None,
guild: Optional[Snowflake] = None,
published: bool = False,
carousel_items: Optional[Collection[Union[StoreAsset, str]]] = None,
preview_video: Optional[Snowflake] = None,
header_background: Optional[Snowflake] = None,
hero_background: Optional[Snowflake] = None,
hero_video: Optional[Snowflake] = None,
box_art: Optional[Snowflake] = None,
thumbnail: Optional[Snowflake] = None,
header_logo_light: Optional[Snowflake] = None,
header_logo_dark: Optional[Snowflake] = None,
) -> StoreListing:
"""|coro|
Creates a a store listing for this SKU.
Parameters
----------
summary: :class:`str`
The summary of the store listing.
summary_localizations: Optional[Mapping[:class:`Locale`, :class:`str`]]
The summary of the store listing localized to different languages.
description: :class:`str`
The description of the store listing.
description_localizations: Optional[Mapping[:class:`Locale`, :class:`str`]]
The description of the store listing localized to different languages.
tagline: Optional[:class:`str`]
The tagline of the store listing.
tagline_localizations: Optional[Mapping[:class:`Locale`, :class:`str`]]
The tagline of the store listing localized to different languages.
child_skus: Optional[List[:class:`SKU`]]
The child SKUs of the store listing.
guild: Optional[:class:`Guild`]
The guild that the store listing is for.
published: :class:`bool`
Whether the store listing is published.
carousel_items: Optional[List[Union[:class:`StoreAsset`, :class:`str`]]]
A list of carousel items to add to the store listing. These can be store assets or YouTube video IDs.
preview_video: Optional[:class:`StoreAsset`]
The preview video of the store listing.
header_background: Optional[:class:`StoreAsset`]
The header background of the store listing.
hero_background: Optional[:class:`StoreAsset`]
The hero background of the store listing.
hero_video: Optional[:class:`StoreAsset`]
The hero video of the store listing.
box_art: Optional[:class:`StoreAsset`]
The box art of the store listing.
thumbnail: Optional[:class:`StoreAsset`]
The thumbnail of the store listing.
header_logo_light: Optional[:class:`StoreAsset`]
The header logo image for light backgrounds.
header_logo_dark: Optional[:class:`StoreAsset`]
The header logo image for dark backgrounds.
Raises
------
Forbidden
You do not have permissions to edit the store listing.
HTTPException
Editing the store listing failed.
"""
payload: Dict[str, Any] = {
'summary': {
'default': summary or '',
'localizations': {str(k): v for k, v in (summary_localizations or {}).items()},
},
'description': {
'default': description or '',
'localizations': {str(k): v for k, v in (description_localizations or {}).items()},
},
}
if tagline or tagline_localizations:
payload['tagline'] = {
'default': tagline or '',
'localizations': {str(k): v for k, v in (tagline_localizations or {}).items()},
}
if child_skus:
payload['child_sku_ids'] = [sku.id for sku in child_skus]
if guild:
payload['guild_id'] = guild.id
if published:
payload['published'] = True
if carousel_items:
payload['carousel_items'] = [
item.to_carousel_item() if isinstance(item, StoreAsset) else {'youtube_video_id': item}
for item in carousel_items
]
if preview_video:
payload['preview_video_asset_id'] = preview_video.id
if header_background:
payload['header_background_asset_id'] = header_background.id
if hero_background:
payload['hero_background_asset_id'] = hero_background.id
if hero_video:
payload['hero_video_asset_id'] = hero_video.id
if box_art:
payload['box_art_asset_id'] = box_art.id
if thumbnail:
payload['thumbnail_asset_id'] = thumbnail.id
if header_logo_light:
payload['header_logo_light_theme_asset_id'] = header_logo_light.id
if header_logo_dark:
payload['header_logo_dark_theme_asset_id'] = header_logo_dark.id
data = await self._state.http.create_store_listing(self.application_id, self.id, payload)
return StoreListing(data=data, state=self._state, application=self.application)
async def create_discount(self, user: Snowflake, percent_off: int, *, ttl: int = 3600) -> None:
"""|coro|
Creates a discount for this SKU for a user.
This discount will be applied to the user's next purchase of this SKU.
Parameters
----------
user: :class:`User`
The user to create the discount for.
percent_off: :class:`int`
The discount in the form of a percentage off the price to give the user.
ttl: :class:`int`
How long the discount should last for in seconds.
Minimum 60 seconds, maximum 3600 seconds.
Raises
------
Forbidden
You do not have permissions to create the discount.
HTTPException
Creating the discount failed.
"""
await self._state.http.create_sku_discount(self.id, user.id, percent_off, ttl)
async def delete_discount(self, user: Snowflake) -> None:
"""|coro|
Deletes a discount for this SKU for a user.
You do not need to call this after a discounted purchase has been made,
as the discount will be automatically consumed and deleted.
Parameters
----------
user: :class:`User`
The user to delete the discount for.
Raises
------
Forbidden
You do not have permissions to delete the discount.
HTTPException
Deleting the discount failed.
"""
await self._state.http.delete_sku_discount(self.id, user.id)
async def create_gift_batch(
self,
*,
amount: int,
description: str,
entitlement_branches: Optional[List[Snowflake]] = None,
entitlement_starts_at: Optional[date] = None,
entitlement_ends_at: Optional[date] = None,
) -> GiftBatch:
"""|coro|
Creates a gift batch for this SKU.
Parameters
-----------
amount: :class:`int`
The amount of gifts to create in the batch.
description: :class:`str`
The description of the gift batch.
entitlement_branches: List[:class:`ApplicationBranch`]
The branches to grant in the gifts.
entitlement_starts_at: :class:`datetime.date`
When the entitlement is valid from.
entitlement_ends_at: :class:`datetime.date`
When the entitlement is valid until.
Raises
------
Forbidden
You do not have permissions to create a gift batch.
HTTPException
Creating the gift batch failed.
Returns
-------
:class:`GiftBatch`
The gift batch created.
"""
from .entitlements import GiftBatch
state = self._state
app_id = self.application_id
data = await state.http.create_gift_batch(
app_id,
self.id,
amount,
description,
entitlement_branches=[branch.id for branch in entitlement_branches] if entitlement_branches else None,
entitlement_starts_at=entitlement_starts_at.isoformat() if entitlement_starts_at else None,
entitlement_ends_at=entitlement_ends_at.isoformat() if entitlement_ends_at else None,
)
return GiftBatch(data=data, state=state, application_id=app_id)
async def gifts(self, subscription_plan: Optional[Snowflake] = None) -> List[Gift]:
"""|coro|
Retrieves the gifts purchased for this SKU.
Parameters
----------
subscription_plan: Optional[:class:`SubscriptionPlan`]
The subscription plan to retrieve the gifts for.
Raises
------
HTTPException
Retrieving the gifts failed.
Returns
-------
List[:class:`Gift`]
The gifts that have been purchased for this SKU.
"""
from .entitlements import Gift
data = await self._state.http.get_sku_gifts(self.id, subscription_plan.id if subscription_plan else None)
return [Gift(data=gift, state=self._state) for gift in data]
async def create_gift(
self, *, subscription_plan: Optional[Snowflake] = None, gift_style: Optional[GiftStyle] = None
) -> Gift:
"""|coro|
Creates a gift for this SKU.
You must have a giftable entitlement for this SKU to create a gift.
Parameters
-----------
subscription_plan: Optional[:class:`SubscriptionPlan`]
The subscription plan to gift.
gift_style: Optional[:class:`GiftStyle`]
The style of the gift.
Raises
------
Forbidden
You do not have permissions to create a gift.
HTTPException
Creating the gift failed.
Returns
-------
:class:`Gift`
The gift created.
"""
from .entitlements import Gift
state = self._state
data = await state.http.create_gift(
self.id,
subscription_plan_id=subscription_plan.id if subscription_plan else None,
gift_style=int(gift_style) if gift_style else None,
)
return Gift(data=data, state=state)
async def preview_purchase(
self, payment_source: Snowflake, *, subscription_plan: Optional[Snowflake] = None, test_mode: bool = False
) -> SKUPrice:
"""|coro|
Previews a purchase of this SKU.
Parameters
----------
payment_source: :class:`PaymentSource`
The payment source to use for the purchase.
subscription_plan: Optional[:class:`SubscriptionPlan`]
The subscription plan being purchased.
test_mode: :class:`bool`
Whether to preview the purchase in test mode.
Raises
------
HTTPException
Previewing the purchase failed.
Returns
-------
:class:`SKUPrice`
The previewed purchase price.
"""
data = await self._state.http.preview_sku_purchase(
self.id, payment_source.id, subscription_plan.id if subscription_plan else None, test_mode=test_mode
)
return SKUPrice(data=data)
async def purchase(
self,
payment_source: Optional[Snowflake] = None,
*,
subscription_plan: Optional[Snowflake] = None,
expected_amount: Optional[int] = None,
expected_currency: Optional[str] = None,
gift: bool = False,
gift_style: Optional[GiftStyle] = None,
test_mode: bool = False,
payment_source_token: Optional[str] = None,
purchase_token: Optional[str] = None,
return_url: Optional[str] = None,
gateway_checkout_context: Optional[str] = None,
) -> Tuple[List[Entitlement], List[LibraryApplication], Optional[Gift]]:
"""|coro|
Purchases this SKU.
Parameters
----------
payment_source: Optional[:class:`PaymentSource`]
The payment source to use for the purchase.
Not required for free SKUs.
subscription_plan: Optional[:class:`SubscriptionPlan`]
The subscription plan to purchase.
Can only be used for premium subscription SKUs.
expected_amount: Optional[:class:`int`]
The expected amount of the purchase.
This can be gotten from :attr:`price` or :meth:`preview_purchase`.
If the value passed here does not match the actual purchase amount,
the purchase will error.
expected_currency: Optional[:class:`str`]
The expected currency of the purchase.
This can be gotten from :attr:`price` or :meth:`preview_purchase`.
If the value passed here does not match the actual purchase currency,
the purchase will error.
gift: :class:`bool`
Whether to purchase the SKU as a gift.
Certain requirements must be met for this to be possible.
gift_style: Optional[:class:`GiftStyle`]
The style of the gift. Only applicable if ``gift`` is ``True``.
test_mode: :class:`bool`
Whether to purchase the SKU in test mode.
payment_source_token: Optional[:class:`str`]
The token used to authorize with the payment source.
purchase_token: Optional[:class:`str`]
The purchase token to use.
return_url: Optional[:class:`str`]
The URL to return to after the payment is complete.
gateway_checkout_context: Optional[:class:`str`]
The current checkout context.
Raises
------
TypeError
``gift_style`` was passed but ``gift`` was not ``True``.
HTTPException
Purchasing the SKU failed.
Returns
-------
Tuple[List[:class:`Entitlement`], List[:class:`LibraryApplication`], Optional[:class:`Gift`]]
The purchased entitlements, the library entries created, and the gift created (if any).
"""
if not gift and gift_style:
raise TypeError('gift_style can only be used with gifts')
state = self._state
data = await state.http.purchase_sku(
self.id,
payment_source.id if payment_source else None,
subscription_plan_id=subscription_plan.id if subscription_plan else None,
expected_amount=expected_amount,
expected_currency=expected_currency,
gift=gift,
gift_style=int(gift_style) if gift_style else None,
test_mode=test_mode,
payment_source_token=payment_source_token,
purchase_token=purchase_token,
return_url=return_url,
gateway_checkout_context=gateway_checkout_context,
)
from .entitlements import Entitlement, Gift
from .library import LibraryApplication
entitlements = [Entitlement(state=state, data=entitlement) for entitlement in data.get('entitlements', [])]
library_applications = [
LibraryApplication(state=state, data=application) for application in data.get('library_applications', [])
]
gift_code = data.get('gift_code')
gift_ = None
if gift_code:
# We create fake gift data
gift_data: GiftPayload = {
'code': gift_code,
'application_id': self.application_id,
'subscription_plan_id': subscription_plan.id if subscription_plan else None,
'sku_id': self.id,
'gift_style': int(gift_style) if gift_style else None, # type: ignore # Enum is identical
'max_uses': 1,
'uses': 0,
'user': state.user._to_minimal_user_json(), # type: ignore
}
gift_ = Gift(state=state, data=gift_data)
if subscription_plan and isinstance(subscription_plan, SubscriptionPlan):
gift_.subscription_plan = subscription_plan
return entitlements, library_applications, gift_
class SubscriptionPlanPrices:
"""Represents the different prices for a :class:`SubscriptionPlan`.
.. versionadded:: 2.0
Attributes
----------
country_code: :class:`str`
The country code the country prices are for.
country_prices: List[:class:`SKUPrice`]
The prices for the country the plan is being purchased in.
payment_source_prices: Dict[:class:`int`, List[:class:`SKUPrice`]]
A mapping of payment source IDs to the prices for that payment source.
"""
def __init__(self, data: SubscriptionPricesPayload):
country_prices = data.get('country_prices') or {}
payment_source_prices = data.get('payment_source_prices') or {}
self.country_code: str = country_prices.get('country_code', 'US')
self.country_prices: List[SKUPrice] = [SKUPrice(data=price) for price in country_prices.get('prices', [])]
self.payment_source_prices: Dict[int, List[SKUPrice]] = {
int(payment_source_id): [SKUPrice(data=price) for price in prices]
for payment_source_id, prices in payment_source_prices.items()
}
def __repr__(self) -> str:
return f'<SubscriptionPlanPrice country_code={self.country_code!r}>'
class SubscriptionPlan(Hashable):
"""Represents a subscription plan for a :class:`SKU`.
.. container:: operations
.. describe:: x == y
Checks if two subscription plans are equal.
.. describe:: x != y
Checks if two subscription plans are not equal.
.. describe:: hash(x)
Returns the subscription plan's hash.
.. describe:: str(x)
Returns the subscription plan's name.
.. versionadded:: 2.0
Attributes
----------
id: :class:`int`
The ID of the subscription plan.
name: :class:`str`
The name of the subscription plan.
sku_id: :class:`int`
The ID of the SKU that this subscription plan is for.
interval: :class:`SubscriptionInterval`
The interval of the subscription plan.
interval_count: :class:`int`
The number of intervals that make up a subscription period.
tax_inclusive: :class:`bool`
Whether the subscription plan price is tax inclusive.
prices: Dict[:class:`SubscriptionPlanPurchaseType`, :class:`SubscriptionPlanPrices`]
The different prices of the subscription plan.
Not available in some contexts.
currency: Optional[:class:`str`]
The currency of the subscription plan's price.
Not available in some contexts.
price: Optional[:class:`int`]
The price of the subscription plan.
Not available in some contexts.
discount_price: Optional[:class:`int`]
The discounted price of the subscription plan.
This price is the one premium subscribers will pay, and is only available for premium subscribers.
fallback_currency: Optional[:class:`str`]
The fallback currency of the subscription plan's price.
This is the currency that will be used for gifting if the user's currency is not giftable.
fallback_price: Optional[:class:`int`]
The fallback price of the subscription plan.
This is the price that will be used for gifting if the user's currency is not giftable.
fallback_discount_price: Optional[:class:`int`]
The fallback discounted price of the subscription plan.
This is the discounted price that will be used for gifting if the user's currency is not giftable.
"""
_INTERVAL_TABLE = {
SubscriptionInterval.day: 1,
SubscriptionInterval.month: 30,
SubscriptionInterval.year: 365,
}
__slots__ = (
'id',
'name',
'sku_id',
'interval',
'interval_count',
'tax_inclusive',
'prices',
'currency',
'price_tier',
'price',
'discount_price',
'fallback_currency',
'fallback_price',
'fallback_discount_price',
'_state',
)
def __init__(
self, *, data: Union[PartialSubscriptionPlanPayload, SubscriptionPlanPayload], state: ConnectionState
) -> None:
self._state = state
self._update(data)
def _update(self, data: Union[PartialSubscriptionPlanPayload, SubscriptionPlanPayload]) -> None:
self.id: int = int(data['id'])
self.name: str = data['name']
self.sku_id: int = int(data['sku_id'])
self.interval: SubscriptionInterval = try_enum(SubscriptionInterval, data['interval'])
self.interval_count: int = data['interval_count']
self.tax_inclusive: bool = data['tax_inclusive']
self.prices: Dict[SubscriptionPlanPurchaseType, SubscriptionPlanPrices] = {
try_enum(SubscriptionPlanPurchaseType, int(purchase_type)): SubscriptionPlanPrices(data=price_data)
for purchase_type, price_data in (data.get('prices') or {}).items()
}
self.currency: Optional[str] = data.get('currency')
self.price_tier: Optional[int] = data.get('price_tier')
self.price: Optional[int] = data.get('price')
self.discount_price: Optional[int] = data.get('discount_price')
self.fallback_currency: Optional[str] = data.get('fallback_currency')
self.fallback_price: Optional[int] = data.get('fallback_price')
self.fallback_discount_price: Optional[int] = data.get('fallback_discount_price')
def __repr__(self) -> str:
return f'<SubscriptionPlan id={self.id} name={self.name!r} sku_id={self.sku_id} interval={self.interval!r} interval_count={self.interval_count}>'
def __str__(self) -> str:
return self.name
@property
def duration(self) -> timedelta:
""":class:`datetime.timedelta`: How long the subscription plan lasts."""
return timedelta(days=self.interval_count * self._INTERVAL_TABLE[self.interval])
@property
def premium_type(self) -> Optional[PremiumType]:
"""Optional[:class:`PremiumType`]: The premium type of the subscription plan, if it is a premium subscription."""
return PremiumType.from_sku_id(self.sku_id)
async def gifts(self) -> List[Gift]:
"""|coro|
Retrieves the gifts purchased for this subscription plan.
Raises
------
HTTPException
Retrieving the gifts failed.
Returns
-------
List[:class:`Gift`]
The gifts that have been purchased for this SKU.
"""
from .entitlements import Gift
data = await self._state.http.get_sku_gifts(self.sku_id, self.id)
return [Gift(data=gift, state=self._state) for gift in data]
async def create_gift(self, *, gift_style: Optional[GiftStyle] = None) -> Gift:
"""|coro|
Creates a gift for this subscription plan.
You must have a giftable entitlement for this subscription plan to create a gift.
Parameters
-----------
gift_style: Optional[:class:`GiftStyle`]
The style of the gift.
Raises
------
Forbidden
You do not have permissions to create a gift.
HTTPException
Creating the gift failed.
Returns
-------
:class:`Gift`
The gift created.
"""
from .entitlements import Gift
state = self._state
data = await state.http.create_gift(
self.sku_id,
subscription_plan_id=self.id,
gift_style=int(gift_style) if gift_style else None,
)
return Gift(data=data, state=state)
async def preview_purchase(self, payment_source: Snowflake, *, test_mode: bool = False) -> SKUPrice:
"""|coro|
Previews a purchase of this subscription plan.
Parameters
----------
payment_source: :class:`PaymentSource`
The payment source to use for the purchase.
test_mode: :class:`bool`
Whether to preview the purchase in test mode.
Raises
------
HTTPException
Previewing the purchase failed.
Returns
-------
:class:`SKUPrice`
The previewed purchase price.
"""
data = await self._state.http.preview_sku_purchase(self.id, payment_source.id, self.id, test_mode=test_mode)
return SKUPrice(data=data)
async def purchase(
self,
payment_source: Optional[Snowflake] = None,
*,
expected_amount: Optional[int] = None,
expected_currency: Optional[str] = None,
gift: bool = False,
gift_style: Optional[GiftStyle] = None,
test_mode: bool = False,
payment_source_token: Optional[str] = None,
purchase_token: Optional[str] = None,
return_url: Optional[str] = None,
gateway_checkout_context: Optional[str] = None,
) -> Tuple[List[Entitlement], List[LibraryApplication], Optional[Gift]]:
"""|coro|
Purchases this subscription plan.
This can only be used on premium subscription plans.
Parameters
----------
payment_source: Optional[:class:`PaymentSource`]
The payment source to use for the purchase.
Not required for free subscription plans.
expected_amount: Optional[:class:`int`]
The expected amount of the purchase.
This can be gotten from :attr:`price` or :meth:`preview_purchase`.
If the value passed here does not match the actual purchase amount,
the purchase will error.
expected_currency: Optional[:class:`str`]
The expected currency of the purchase.
This can be gotten from :attr:`price` or :meth:`preview_purchase`.
If the value passed here does not match the actual purchase currency,
the purchase will error.
gift: :class:`bool`
Whether to purchase the subscription plan as a gift.
Certain requirements must be met for this to be possible.
gift_style: Optional[:class:`GiftStyle`]
The style of the gift. Only applicable if ``gift`` is ``True``.
test_mode: :class:`bool`
Whether to purchase the subscription plan in test mode.
payment_source_token: Optional[:class:`str`]
The token used to authorize with the payment source.
purchase_token: Optional[:class:`str`]
The purchase token to use.
return_url: Optional[:class:`str`]
The URL to return to after the payment is complete.
gateway_checkout_context: Optional[:class:`str`]
The current checkout context.
Raises
------
TypeError
``gift_style`` was passed but ``gift`` was not ``True``.
HTTPException
Purchasing the subscription plan failed.
Returns
-------
Tuple[List[:class:`Entitlement`], List[:class:`LibraryApplication`], Optional[:class:`Gift`]]
The purchased entitlements, the library entries created, and the gift created (if any).
"""
if not gift and gift_style:
raise TypeError('gift_style can only be used with gifts')
state = self._state
data = await self._state.http.purchase_sku(
self.sku_id,
payment_source.id if payment_source else None,
subscription_plan_id=self.id,
expected_amount=expected_amount,
expected_currency=expected_currency,
gift=gift,
gift_style=int(gift_style) if gift_style else None,
test_mode=test_mode,
payment_source_token=payment_source_token,
purchase_token=purchase_token,
return_url=return_url,
gateway_checkout_context=gateway_checkout_context,
)
from .entitlements import Entitlement, Gift
from .library import LibraryApplication
entitlements = [Entitlement(state=state, data=entitlement) for entitlement in data.get('entitlements', [])]
library_applications = [
LibraryApplication(state=state, data=application) for application in data.get('library_applications', [])
]
gift_code = data.get('gift_code')
gift_ = None
if gift_code:
# We create fake gift data
gift_data: GiftPayload = {
'code': gift_code,
'subscription_plan_id': self.id,
'sku_id': self.sku_id,
'gift_style': int(gift_style) if gift_style else None, # type: ignore # Enum is identical
'max_uses': 1,
'uses': 0,
'user': state.user._to_minimal_user_json(), # type: ignore
}
gift_ = Gift(state=state, data=gift_data)
gift_.subscription_plan = self
return entitlements, library_applications, gift_