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
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_
|
|
|