Browse Source

Add new store fields and separate types

pull/10109/head
dolfies 1 year ago
parent
commit
c507bc2524
  1. 15
      discord/enums.py
  2. 17
      discord/flags.py
  3. 24
      discord/http.py
  4. 74
      discord/store.py
  5. 19
      discord/subscriptions.py
  6. 6
      discord/types/entitlements.py
  7. 4
      discord/types/payments.py
  8. 69
      discord/types/store.py
  9. 3
      discord/types/subscriptions.py
  10. 6
      docs/api.rst

15
discord/enums.py

@ -107,6 +107,7 @@ __all__ = (
'ApplicationAssetType',
'SKUType',
'SKUAccessLevel',
'SKUProductLine',
'SKUFeature',
'SKUGenre',
'OperatingSystem',
@ -1188,6 +1189,7 @@ class PaymentSourceType(Enum):
bancontact = 14
eps = 15
ideal = 16
cash_app = 17
payment_request = 99
@ -1290,6 +1292,19 @@ class SKUAccessLevel(Enum, comparable=True):
return self.value
class SKUProductLine(Enum):
premium = 1
guild_premium = 2
iap = 3
guild_role = 4
guild_product = 5
application = 6
collectible = 7
def __int__(self) -> int:
return self.value
class SKUFeature(Enum):
single_player = 1
online_multiplayer = 2

17
discord/flags.py

@ -1591,6 +1591,23 @@ class SKUFlags(BaseFlags):
""":class:`bool`: Returns ``True`` if the SKU is a application subscription. These are subscriptions made to applications for premium perks bound to a user."""
return 1 << 8
@flag_value
def creator_monetization(self):
""":class:`bool`: Returns ``True`` if the SKU is a creator monetization product (e.g. guild role subscription, guild product).
.. versionadded:: 2.1
"""
# For some reason this is only actually present on products...
return 1 << 9
@flag_value
def guild_product(self):
""":class:`bool`: Returns ``True`` if the SKU is a guild product. These are one-time purchases made by guilds for premium perks.
.. versionadded:: 2.1
"""
return 1 << 10
@fill_with_flags()
class PaymentFlags(BaseFlags):

24
discord/http.py

@ -3090,7 +3090,7 @@ class HTTPClient:
payment_source_id: Optional[Snowflake] = None,
localize: bool = True,
with_bundled_skus: bool = True,
) -> Response[List[store.SKU]]:
) -> Response[List[store.PrivateSKU]]:
params = {}
if country_code:
params['country_code'] = country_code
@ -3105,7 +3105,7 @@ class HTTPClient:
Route('GET', '/applications/{app_id}/skus', app_id=app_id), params=params, super_properties_to_track=True
)
def create_sku(self, payload: dict) -> Response[store.SKU]:
def create_sku(self, payload: dict) -> Response[store.PrivateSKU]:
return self.request(Route('POST', '/store/skus'), json=payload, super_properties_to_track=True)
def get_app_discoverability(self, app_id: Snowflake) -> Response[application.ApplicationDiscoverability]:
@ -3507,7 +3507,7 @@ class HTTPClient:
country_code: Optional[str] = None,
payment_source_id: Optional[Snowflake] = None,
localize: bool = True,
) -> Response[store.StoreListing]:
) -> Response[store.PrivateStoreListing]:
params = {}
if country_code:
params['country_code'] = country_code
@ -3525,7 +3525,7 @@ class HTTPClient:
country_code: Optional[str] = None,
payment_source_id: Optional[Snowflake] = None,
localize: bool = True,
) -> Response[store.StoreListing]:
) -> Response[store.PublicStoreListing]:
params = {}
if country_code:
params['country_code'] = country_code
@ -3543,7 +3543,7 @@ class HTTPClient:
country_code: Optional[str] = None,
payment_source_id: Optional[int] = None,
localize: bool = True,
) -> Response[List[store.StoreListing]]:
) -> Response[List[store.PrivateStoreListing]]:
params = {}
if country_code:
params['country_code'] = country_code
@ -3603,7 +3603,7 @@ class HTTPClient:
country_code: Optional[str] = None,
payment_source_id: Optional[int] = None,
localize: bool = True,
) -> Response[List[store.StoreListing]]:
) -> Response[List[store.PublicStoreListing]]:
params = {'application_id': app_id}
if country_code:
params['country_code'] = country_code
@ -3621,7 +3621,7 @@ class HTTPClient:
country_code: Optional[str] = None,
payment_source_id: Optional[int] = None,
localize: bool = True,
) -> Response[store.StoreListing]:
) -> Response[store.PublicStoreListing]:
params = {}
if country_code:
params['country_code'] = country_code
@ -3641,7 +3641,7 @@ class HTTPClient:
country_code: Optional[str] = None,
payment_source_id: Optional[Snowflake] = None,
localize: bool = True,
) -> Response[List[store.StoreListing]]:
) -> Response[List[store.PublicStoreListing]]:
params: Dict[str, Any] = {'application_ids': app_ids}
if country_code:
params['country_code'] = country_code
@ -3654,14 +3654,14 @@ class HTTPClient:
def create_store_listing(
self, application_id: Snowflake, sku_id: Snowflake, payload: dict
) -> Response[store.StoreListing]:
) -> Response[store.PrivateStoreListing]:
return self.request(
Route('POST', '/store/listings'),
json={**payload, 'application_id': application_id, 'sku_id': sku_id},
super_properties_to_track=True,
)
def edit_store_listing(self, listing_id: Snowflake, payload: dict) -> Response[store.StoreListing]:
def edit_store_listing(self, listing_id: Snowflake, payload: dict) -> Response[store.PrivateStoreListing]:
return self.request(
Route('PATCH', '/store/listings/{listing_id}', listing_id=listing_id),
json=payload,
@ -3675,7 +3675,7 @@ class HTTPClient:
country_code: Optional[str] = None,
payment_source_id: Optional[Snowflake] = None,
localize: bool = True,
) -> Response[store.SKU]:
) -> Response[store.PrivateSKU]:
params = {}
if country_code:
params['country_code'] = country_code
@ -3686,7 +3686,7 @@ class HTTPClient:
return self.request(Route('GET', '/store/skus/{sku_id}', sku_id=sku_id), params=params)
def edit_sku(self, sku_id: Snowflake, payload: dict) -> Response[store.SKU]:
def edit_sku(self, sku_id: Snowflake, payload: dict) -> Response[store.PrivateSKU]:
return self.request(
Route('PATCH', '/store/skus/{sku_id}', sku_id=sku_id), json=payload, super_properties_to_track=True
)

74
discord/store.py

@ -41,6 +41,7 @@ from .enums import (
SKUAccessLevel,
SKUFeature,
SKUGenre,
SKUProductLine,
SKUType,
SubscriptionInterval,
SubscriptionPlanPurchaseType,
@ -76,6 +77,7 @@ if TYPE_CHECKING:
SKU as SKUPayload,
CarouselItem as CarouselItemPayload,
ContentRating as ContentRatingPayload,
PremiumPrice as PremiumPricePayload,
SKUPrice as SKUPricePayload,
StoreListing as StoreListingPayload,
StoreNote as StoreNotePayload,
@ -330,6 +332,8 @@ class SystemRequirements:
Any extra notes on recommended requirements.
"""
# I hate this class so much
if TYPE_CHECKING:
os: OperatingSystem
minimum_ram: Optional[int]
@ -607,6 +611,19 @@ class StoreListing(Hashable):
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`]
@ -645,6 +662,8 @@ class StoreListing(Hashable):
'entitlement_branch_id',
'guild',
'published',
'published_at',
'unpublished_at',
'staff_note',
'assets',
'carousel_items',
@ -693,6 +712,8 @@ class StoreListing(Hashable):
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
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
)
@ -902,22 +923,35 @@ class SKUPrice:
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', 'premium', 'exponent')
__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.premium = data.get('premium')
self.exponent: Optional[int] = data.get('exponent')
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'),
}
@ -925,6 +959,17 @@ class SKUPrice:
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}>'
@ -939,8 +984,13 @@ class SKUPrice:
return self.sale_percentage > 0
def is_free(self) -> bool:
""":class:`bool`: Checks whether the SKU is free."""
return self.amount == 0
""":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:
@ -1047,6 +1097,10 @@ class SKU(Hashable):
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`]
@ -1098,6 +1152,10 @@ class SKU(Hashable):
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`]
@ -1116,6 +1174,7 @@ class SKU(Hashable):
'legal_notice',
'legal_notice_localizations',
'type',
'product_line',
'slug',
'price_tier',
'price_overrides',
@ -1139,6 +1198,7 @@ class SKU(Hashable):
'premium',
'restricted',
'exclusive',
'deleted',
'show_age_gate',
'bundled_skus',
'manifests',
@ -1179,6 +1239,9 @@ class SKU(Hashable):
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'])
@ -1233,6 +1296,7 @@ class SKU(Hashable):
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', [])

19
discord/subscriptions.py

@ -201,9 +201,13 @@ class SubscriptionInvoiceItem(Hashable):
The price of the subscription plan the item represents. This does not include discounts.
discounts: List[:class:`SubscriptionDiscount`]
A list of discounts applied to the item.
metadata: :class:`Metadata`
Extra metadata about the invoice item.
.. versionadded:: 2.1
"""
__slots__ = ('id', 'quantity', 'amount', 'proration', 'plan_id', 'plan_price', 'discounts')
__slots__ = ('id', 'quantity', 'amount', 'proration', 'plan_id', 'plan_price', 'discounts', 'metadata')
def __init__(self, data: SubscriptionInvoiceItemPayload) -> None:
self.id: int = int(data['id'])
@ -213,6 +217,7 @@ class SubscriptionInvoiceItem(Hashable):
self.plan_id: int = int(data['subscription_plan_id'])
self.plan_price: int = data['subscription_plan_price']
self.discounts: List[SubscriptionDiscount] = [SubscriptionDiscount(d) for d in data['discounts']]
self.metadata: Metadata = Metadata(data.get('tenant_metadata', {}))
def __repr__(self) -> str:
return f'<SubscriptionInvoiceItem id={self.id} quantity={self.quantity} amount={self.amount}>'
@ -277,6 +282,10 @@ class SubscriptionInvoice(Hashable):
When the current billing period started.
current_period_end: :class:`datetime.datetime`
When the current billing period ends.
applied_discount_ids: List[:class:`int`]
The IDs of the discounts applied to the invoice.
.. versionadded:: 2.1
"""
__slots__ = (
@ -292,6 +301,8 @@ class SubscriptionInvoice(Hashable):
'items',
'current_period_start',
'current_period_end',
'applied_discount_ids',
'applied_user_discounts',
)
def __init__(
@ -316,6 +327,12 @@ class SubscriptionInvoice(Hashable):
self.current_period_start: datetime = parse_time(data['subscription_period_start']) # type: ignore # Should always be a datetime
self.current_period_end: datetime = parse_time(data['subscription_period_end']) # type: ignore # Should always be a datetime
# These fields are unknown
self.applied_discount_ids: List[int] = [int(id) for id in data.get('applied_discount_ids', [])]
self.applied_user_discounts: Dict[int, Optional[Any]] = {
int(k): v for k, v in data.get('applied_user_discounts', {}).items()
}
def __repr__(self) -> str:
return f'<SubscriptionInvoice id={self.id} status={self.status!r} total={self.total}>'

6
discord/types/entitlements.py

@ -30,7 +30,7 @@ from typing_extensions import NotRequired
from .payments import PartialPayment
from .promotions import Promotion
from .snowflake import Snowflake
from .store import SKU, StoreListing
from .store import PublicSKU, PublicStoreListing
from .subscriptions import PartialSubscriptionPlan, SubscriptionPlan, SubscriptionTrial
from .user import PartialUser
@ -55,7 +55,7 @@ class Entitlement(TypedDict):
ends_at: NotRequired[str]
subscription_id: NotRequired[Snowflake]
subscription_plan: NotRequired[PartialSubscriptionPlan]
sku: NotRequired[SKU]
sku: NotRequired[PublicSKU]
payment: NotRequired[PartialPayment]
@ -78,7 +78,7 @@ class Gift(GatewayGift):
uses: int
redeemed: bool
revoked: NotRequired[bool]
store_listing: NotRequired[StoreListing]
store_listing: NotRequired[PublicStoreListing]
promotion: NotRequired[Promotion]
subscription_trial: NotRequired[SubscriptionTrial]
subscription_plan: NotRequired[SubscriptionPlan]

4
discord/types/payments.py

@ -29,7 +29,7 @@ from typing_extensions import NotRequired
from .billing import PartialPaymentSource
from .snowflake import Snowflake
from .store import SKU
from .store import PublicSKU
from .subscriptions import PartialSubscription
@ -56,6 +56,6 @@ class Payment(PartialPayment):
downloadable_refund_invoices: NotRequired[List[str]]
refund_disqualification_reasons: NotRequired[List[str]]
flags: int
sku: NotRequired[SKU]
sku: NotRequired[PublicSKU]
payment_source: NotRequired[PartialPaymentSource]
subscription: NotRequired[PartialSubscription]

69
discord/types/store.py

@ -64,16 +64,14 @@ class CarouselItem(TypedDict, total=False):
youtube_video_id: str
class StoreListing(TypedDict):
class BaseStoreListing(TypedDict):
id: Snowflake
summary: NotRequired[LOCALIZED_STR]
description: NotRequired[LOCALIZED_STR]
tagline: NotRequired[LOCALIZED_STR]
flavor_text: NotRequired[str]
published: NotRequired[bool]
entitlement_branch_id: NotRequired[Snowflake]
staff_notes: NotRequired[StoreNote]
guild: NotRequired[PartialGuild]
assets: NotRequired[List[StoreAsset]]
carousel_items: NotRequired[List[CarouselItem]]
preview_video: NotRequired[StoreAsset]
@ -84,17 +82,36 @@ class StoreListing(TypedDict):
thumbnail: NotRequired[StoreAsset]
header_logo_light_theme: NotRequired[StoreAsset]
header_logo_dark_theme: NotRequired[StoreAsset]
sku: SKU
child_skus: NotRequired[List[SKU]]
alternative_skus: NotRequired[List[SKU]]
sku: PublicSKU
child_skus: NotRequired[List[PublicSKU]]
alternative_skus: NotRequired[List[PublicSKU]]
published_at: NotRequired[str]
unpublished_at: NotRequired[str]
class PublicStoreListing(BaseStoreListing):
guild: NotRequired[PartialGuild]
class PrivateStoreListing(BaseStoreListing):
published: bool
StoreListing = Union[PublicStoreListing, PrivateStoreListing]
class PremiumPrice(TypedDict):
amount: int
percentage: int
class SKUPrice(TypedDict):
currency: str
currency_exponent: int
amount: int
sale_amount: NotRequired[Optional[int]]
sale_percentage: NotRequired[int]
premium: NotRequired[bool]
premium: NotRequired[Dict[Literal[1, 2, 3], PremiumPrice]]
class ContentRating(TypedDict):
@ -102,17 +119,21 @@ class ContentRating(TypedDict):
descriptors: List[int]
SKUType = Literal[1, 2, 3, 4, 5, 6]
class PartialSKU(TypedDict):
id: Snowflake
type: Literal[1, 2, 3, 4, 5, 6]
type: SKUType
premium: bool
preorder_release_date: Optional[str]
preorder_released_at: Optional[str]
class SKU(PartialSKU):
class BaseSKU(PartialSKU):
id: Snowflake
type: Literal[1, 2, 3, 4, 5, 6]
type: SKUType
product_line: Optional[Literal[1, 2, 3, 4, 5, 6, 7]]
name: LOCALIZED_STR
summary: NotRequired[LOCALIZED_STR]
legal_notice: NotRequired[LOCALIZED_STR]
@ -121,18 +142,11 @@ class SKU(PartialSKU):
application_id: Snowflake
application: NotRequired[PartialApplication]
flags: int
price_tier: NotRequired[int]
price: NotRequired[Union[SKUPrice, Dict[str, int]]]
sale_price_tier: NotRequired[int]
sale_price: NotRequired[Dict[str, int]]
access_level: Literal[1, 2, 3]
features: List[int]
locales: NotRequired[List[str]]
genres: NotRequired[List[int]]
available_regions: NotRequired[List[str]]
content_rating_agency: NotRequired[Literal[1, 2]]
content_rating: NotRequired[ContentRating]
content_ratings: NotRequired[Dict[Literal[1, 2], ContentRating]]
system_requirements: NotRequired[Dict[Literal[1, 2, 3], SystemRequirements]]
release_date: Optional[str]
preorder_release_date: NotRequired[Optional[str]]
@ -141,11 +155,30 @@ class SKU(PartialSKU):
premium: NotRequired[bool]
restricted: NotRequired[bool]
exclusive: NotRequired[bool]
deleted: NotRequired[bool]
show_age_gate: bool
bundled_skus: NotRequired[List[SKU]]
manifest_labels: Optional[List[Snowflake]]
class PublicSKU(BaseSKU):
bundled_skus: NotRequired[List[PublicSKU]]
price: SKUPrice
content_rating_agency: NotRequired[Literal[1, 2]]
content_rating: NotRequired[ContentRating]
class PrivateSKU(BaseSKU):
bundled_skus: NotRequired[List[PrivateSKU]]
price_tier: int
price: Dict[str, int]
sale_price_tier: int
sale_price: Dict[str, int]
content_ratings: Dict[Literal[1, 2], ContentRating]
SKU = Union[PublicSKU, PrivateSKU]
class SKUPurchase(TypedDict):
entitlements: List[Entitlement]
library_applications: NotRequired[List[LibraryApplication]]

3
discord/types/subscriptions.py

@ -73,6 +73,7 @@ class SubscriptionInvoiceItem(TypedDict):
subscription_plan_id: Snowflake
subscription_plan_price: int
discounts: List[SubscriptionDiscount]
tenant_metadata: NotRequired[Dict[str, Any]]
class SubscriptionInvoice(TypedDict):
@ -86,6 +87,8 @@ class SubscriptionInvoice(TypedDict):
items: List[SubscriptionInvoiceItem]
current_period_start: str
current_period_end: str
applied_discount_ids: NotRequired[List[Snowflake]]
applied_user_discounts: NotRequired[Dict[Snowflake, Optional[Any]]]
class SubscriptionRenewalMutations(TypedDict, total=False):

6
docs/api.rst

@ -3960,6 +3960,12 @@ of :class:`enum.Enum`.
The payment source is an iDEAL account.
.. attribute:: cash_app
The payment source is a Cash App account.
.. versionadded:: 2.1
.. class:: PaymentGateway
Represents the payment gateway used for a payment source.

Loading…
Cancel
Save