Browse Source

Implement (almost) the entirety of the applications, billing, and store API (#449)

* Initial implementation

* Expose various rich methods to fetch the new models

* Add localize parameters and remove useless payment_source parameters from front-facing fetch methods

* Implement fetching and (partially) redeeming gifts

* Slot remaining models

* Correctly document Gift.redeem() channel parameter

* Implement some stuffs, fix more stuffs, add creating/editing skus/store listings

* Various context properties fixes

* Fix various types, expose SubscriptionPlan

* (Partially) implement purchasing SKUs and gift flags

* Finish and clean-up store/applications API implementations

* Implement build uls, missing sub plan params, purchase sku ret

* Fix upload_files() warning

* Formatter pass

* Normalize include_x to with_x, add various small missing things

* Update sub on manual invoice payment instead of returning new object

* Black pass

* Implement missing integrations/applications API shit

* Implement Application.store_listing_sku_id

* Expose richer subscription metadata guild info

* Implement SKU.system_requirements localization and modification

* Black pass

* Implement premium usage

* Implement application whitelist

* Implement active developer program enrollment

* Readd new team members to cache

* Polishing

* Implement leaving active developer program

* Type everything

* Expose everything

* Implement relationship activity statistics, improve model

* Black pass

* Document everything

* Add crunchyroll connection type (#426)

* Fix type-checking error in PrivateChannel ABC (#427)

* Update required property fetching to new domain

* Pin black to v22.6

* Get pyright to shut up

* Black pass

* Get pyright to shut up
pull/10109/head
dolfies 2 years ago
committed by GitHub
parent
commit
9a6c4e1e35
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 15
      discord/__init__.py
  2. 2
      discord/abc.py
  3. 3686
      discord/appinfo.py
  4. 20
      discord/asset.py
  5. 381
      discord/billing.py
  6. 2
      discord/calls.py
  7. 120
      discord/channel.py
  8. 1454
      discord/client.py
  9. 16
      discord/commands.py
  10. 79
      discord/connections.py
  11. 632
      discord/entitlements.py
  12. 472
      discord/enums.py
  13. 18
      discord/errors.py
  14. 17
      discord/file.py
  15. 610
      discord/flags.py
  16. 177
      discord/guild.py
  17. 320
      discord/guild_premium.py
  18. 1591
      discord/http.py
  19. 160
      discord/integrations.py
  20. 22
      discord/invite.py
  21. 280
      discord/library.py
  22. 8
      discord/member.py
  23. 134
      discord/metadata.py
  24. 6
      discord/modal.py
  25. 290
      discord/payments.py
  26. 67
      discord/profile.py
  27. 306
      discord/promotions.py
  28. 2
      discord/role.py
  29. 156
      discord/state.py
  30. 2281
      discord/store.py
  31. 859
      discord/subscriptions.py
  32. 344
      discord/team.py
  33. 16
      discord/tracking.py
  34. 203
      discord/types/appinfo.py
  35. 78
      discord/types/billing.py
  36. 6
      discord/types/channel.py
  37. 95
      discord/types/entitlements.py
  38. 96
      discord/types/gateway.py
  39. 6
      discord/types/guild.py
  40. 24
      discord/types/integration.py
  41. 6
      discord/types/invite.py
  42. 44
      discord/types/library.py
  43. 61
      discord/types/payments.py
  44. 79
      discord/types/promotions.py
  45. 152
      discord/types/store.py
  46. 161
      discord/types/subscriptions.py
  47. 22
      discord/types/team.py
  48. 12
      discord/types/user.py
  49. 45
      discord/user.py
  50. 107
      discord/utils.py
  51. 2251
      docs/api.rst
  52. 54
      docs/migrating.rst
  53. 1
      pyproject.toml

15
discord/__init__.py

@ -19,15 +19,12 @@ __path__ = __import__('pkgutil').extend_path(__path__, __name__)
import logging
from typing import Literal, NamedTuple
from . import (
abc as abc,
opus as opus,
utils as utils,
)
from . import abc as abc, opus as opus, utils as utils
from .activity import *
from .appinfo import *
from .asset import *
from .audit_logs import *
from .billing import *
from .calls import *
from .channel import *
from .client import *
@ -37,25 +34,31 @@ from .components import *
from .connections import *
from .embeds import *
from .emoji import *
from .entitlements import *
from .enums import *
from .errors import *
from .file import *
from .flags import *
from .guild import *
from .guild_folder import *
from .guild_premium import *
from .handlers import *
from .integrations import *
from .interactions import *
from .invite import *
from .library import *
from .member import *
from .mentions import *
from .message import *
from .metadata import *
from .modal import *
from .object import *
from .partial_emoji import *
from .payments import *
from .permissions import *
from .player import *
from .profile import *
from .promotions import *
from .raw_models import *
from .reaction import *
from .relationship import *
@ -64,6 +67,8 @@ from .scheduled_event import *
from .settings import *
from .stage_instance import *
from .sticker import *
from .store import *
from .subscriptions import *
from .team import *
from .template import *
from .threads import *

2
discord/abc.py

@ -363,7 +363,7 @@ class PrivateChannel(Snowflake, Protocol):
def _add_call(self, **kwargs):
raise NotImplementedError
def _update(self, **kwargs) -> None:
def _update(self, *args) -> None:
raise NotImplementedError

3686
discord/appinfo.py

File diff suppressed because it is too large

20
discord/asset.py

@ -344,6 +344,26 @@ class Asset(AssetMixin):
animated=animated,
)
@classmethod
def _from_astore_asset(cls, state, app_id: int, hash: str) -> Asset:
animated = hash.startswith('a_')
format = 'gif' if animated else 'png'
return cls(
state,
url=f'{cls.BASE}/app-assets/{app_id}/{hash}.{format}',
key=hash,
animated=animated,
)
@classmethod
def _from_achievement_icon(cls, state, app_id: int, achievement_id: int, icon_hash: str) -> Asset:
return cls(
state,
url=f'{cls.BASE}/app-assets/{app_id}/achievements/{achievement_id}/icons/{icon_hash}.png',
key=icon_hash,
animated=False,
)
def __str__(self) -> str:
return self._url

381
discord/billing.py

@ -0,0 +1,381 @@
"""
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
from typing import TYPE_CHECKING, Optional, Union
from .enums import (
PaymentGateway,
PaymentSourceType,
try_enum,
)
from .flags import PaymentSourceFlags
from .mixins import Hashable
from .utils import MISSING
if TYPE_CHECKING:
from datetime import date
from typing_extensions import Self
from .state import ConnectionState
from .types.billing import (
BillingAddress as BillingAddressPayload,
PartialPaymentSource as PartialPaymentSourcePayload,
PaymentSource as PaymentSourcePayload,
PremiumUsage as PremiumUsagePayload,
)
__all__ = (
'BillingAddress',
'PaymentSource',
'PremiumUsage',
)
class BillingAddress:
"""Represents a billing address.
.. container:: operations
.. describe:: x == y
Checks if two billing addresses are equal.
.. describe:: x != y
Checks if two billing addresses are not equal.
.. describe:: hash(x)
Returns the address' hash.
.. versionadded:: 2.0
Attributes
----------
name: :class:`str`
The payment source's name.
address: :class:`str`
The location's address.
postal_code: Optional[:class:`str`]
The location's postal code.
city: :class:`str`
The location's city.
state: Optional[:class:`str`]
The location's state or province.
country: :class:`str`
The location's country.
email: Optional[:class:`str`]
The email address associated with the payment source, if any.
"""
__slots__ = ('_state', 'name', 'address', 'postal_code', 'city', 'state', 'country', 'email')
def __init__(
self,
*,
name: str,
address: str,
city: str,
country: str,
state: Optional[str] = None,
postal_code: Optional[str] = None,
email: Optional[str] = None,
_state: Optional[ConnectionState] = None,
) -> None:
self._state = _state
self.name = name
self.address = address
self.postal_code = postal_code
self.city = city
self.state = state
self.country = country
self.email = email
def __repr__(self) -> str:
return f'<BillingAddress name={self.name!r} address={self.address!r} city={self.city!r} country={self.country!r}>'
def __eq__(self, other: object) -> bool:
return isinstance(other, BillingAddress) and self.to_dict() == other.to_dict()
def __ne__(self, other: object) -> bool:
if not isinstance(other, BillingAddress):
return True
return self.to_dict() != other.to_dict()
def __hash__(self) -> int:
return hash(self.to_dict())
@classmethod
def from_dict(cls, data: BillingAddressPayload, state: ConnectionState) -> Self:
address = '\n'.join(filter(None, (data['line_1'], data.get('line_2'))))
return cls(
_state=state,
name=data['name'],
address=address,
postal_code=data.get('postal_code'),
city=data['city'],
state=data.get('state'),
country=data['country'],
email=data.get('email'),
)
def to_dict(self) -> dict:
line1, _, line2 = self.address.partition('\n')
data = {
'name': self.name,
'line_1': line1,
'line_2': line2 or '',
'city': self.city,
'country': self.country,
}
if self.postal_code:
data['postal_code'] = self.postal_code
if self.state:
data['state'] = self.state
if self.email:
data['email'] = self.email
return data
async def validate(self) -> str:
"""|coro|
Validates the billing address.
Raises
------
TypeError
The billing address does not have state attached.
HTTPException
The billing address is invalid.
Returns
-------
:class:`str`
The billing address token.
"""
if self._state is None:
raise TypeError('BillingAddress does not have state available')
data = await self._state.http.validate_billing_address(self.to_dict())
return data['token']
class PaymentSource(Hashable):
"""Represents a payment source.
.. container:: operations
.. describe:: x == y
Checks if two payment sources are equal.
.. describe:: x != y
Checks if two payment sources are not equal.
.. describe:: hash(x)
Returns the source's hash.
.. versionadded:: 2.0
Attributes
----------
id: :class:`int`
The ID of the payment source.
brand: Optional[:class:`str`]
The brand of the payment source. This is only available for cards.
country: Optional[:class:`str`]
The country of the payment source. Not available in all contexts.
partial_card_number: Optional[:class:`str`]
The last four digits of the payment source. This is only available for cards.
billing_address: Optional[:class:`BillingAddress`]
The billing address of the payment source. Not available in all contexts.
type: :class:`PaymentSourceType`
The type of the payment source.
payment_gateway: :class:`PaymentGateway`
The payment gateway of the payment source.
default: :class:`bool`
Whether the payment source is the default payment source.
invalid: :class:`bool`
Whether the payment source is invalid.
expires_at: Optional[:class:`datetime.date`]
When the payment source expires. This is only available for cards.
email: Optional[:class:`str`]
The email address associated with the payment source, if any.
This is only available for PayPal.
bank: Optional[:class:`str`]
The bank associated with the payment source, if any.
This is only available for certain payment sources.
username: Optional[:class:`str`]
The username associated with the payment source, if any.
This is only available for Venmo.
"""
__slots__ = (
'_state',
'id',
'brand',
'country',
'partial_card_number',
'billing_address',
'type',
'payment_gateway',
'default',
'invalid',
'expires_at',
'email',
'bank',
'username',
'_flags',
)
def __init__(self, *, data: Union[PaymentSourcePayload, PartialPaymentSourcePayload], state: ConnectionState) -> None:
self._state = state
self._update(data)
def __repr__(self) -> str:
return f'<PaymentSource id={self.id} type={self.type!r} country={self.country!r}>'
def _update(self, data: Union[PaymentSourcePayload, PartialPaymentSourcePayload]) -> None:
self.id: int = int(data['id'])
self.brand: Optional[str] = data.get('brand')
self.country: Optional[str] = data.get('country')
self.partial_card_number: Optional[str] = data.get('last_4')
self.billing_address: Optional[BillingAddress] = (
BillingAddress.from_dict(data['billing_address'], state=self._state) if 'billing_address' in data else None # type: ignore # ???
)
self.type: PaymentSourceType = try_enum(PaymentSourceType, data['type'])
self.payment_gateway: PaymentGateway = try_enum(PaymentGateway, data['payment_gateway'])
self.default: bool = data.get('default', False)
self.invalid: bool = data['invalid']
self._flags: int = data.get('flags', 0)
month = data.get('expires_month')
year = data.get('expires_year')
self.expires_at: Optional[date] = datetime(year=year, month=month or 1, day=1).date() if year else None
self.email: Optional[str] = data.get('email')
self.bank: Optional[str] = data.get('bank')
self.username: Optional[str] = data.get('username')
if not self.country and self.billing_address:
self.country = self.billing_address.country
if not self.email and self.billing_address:
self.email = self.billing_address.email
@property
def flags(self) -> PaymentSourceFlags:
""":class:`PaymentSourceFlags`: Returns the payment source's flags."""
return PaymentSourceFlags._from_value(self._flags)
async def edit(
self, *, billing_address: BillingAddress = MISSING, default: bool = MISSING, expires_at: date = MISSING
) -> None:
"""|coro|
Edits the payment source.
Parameters
----------
billing_address: :class:`BillingAddress`
The billing address of the payment source.
default: :class:`bool`
Whether the payment source is the default payment source.
expires_at: :class:`datetime.date`
When the payment source expires. This is only applicable to cards.
Raises
------
HTTPException
Editing the payment source failed.
"""
payload = {}
if billing_address is not MISSING:
payload['billing_address'] = billing_address.to_dict()
if default is not MISSING:
payload['default'] = default
if expires_at is not MISSING:
payload['expires_month'] = expires_at.month
payload['expires_year'] = expires_at.year
data = await self._state.http.edit_payment_source(self.id, payload)
self._update(data)
async def delete(self) -> None:
"""|coro|
Deletes the payment source.
Raises
------
HTTPException
Deleting the payment source failed.
"""
await self._state.http.delete_payment_source(self.id)
class PremiumUsage:
"""Represents the usage of a user's premium perks.
.. versionadded:: 2.0
Attributes
----------
sticker_sends: :class:`int`
The number of premium sticker sends.
animated_emojis: :class:`int`
The number of animated emojis used.
global_emojis: :class:`int`
The number of global emojis used.
large_uploads: :class:`int`
The number of large uploads made.
hd_streams: :class:`int`
The number of HD streams.
hd_hours_streamed: :class:`int`
The number of hours streamed in HD.
"""
__slots__ = (
'sticker_sends',
'animated_emojis',
'global_emojis',
'large_uploads',
'hd_streams',
'hd_hours_streamed',
)
def __init__(self, *, data: PremiumUsagePayload) -> None:
self.sticker_sends: int = data['nitro_sticker_sends']['value']
self.animated_emojis: int = data['total_animated_emojis']['value']
self.global_emojis: int = data['total_global_emojis']['value']
self.large_uploads: int = data['total_large_uploads']['value']
self.hd_streams: int = data['total_hd_streams']['value']
self.hd_hours_streamed: int = data['hd_hours_streamed']['value']

2
discord/calls.py

@ -109,7 +109,7 @@ class CallMessage:
The timedelta object representing the duration.
"""
if self.ended_timestamp is None:
return datetime.datetime.utcnow() - self.message.created_at
return utils.utcnow() - self.message.created_at
else:
return self.ended_timestamp - self.message.created_at

120
discord/channel.py

@ -2286,7 +2286,7 @@ class DMChannel(discord.abc.Messageable, discord.abc.Connectable, Hashable):
@property
def notification_settings(self) -> ChannelSettings:
""":class:`~discord.ChannelSettings`: Returns the notification settings for this channel.
""":class:`ChannelSettings`: Returns the notification settings for this channel.
If not found, an instance is created with defaults applied. This follows Discord behaviour.
@ -2556,12 +2556,40 @@ class GroupChannel(discord.abc.Messageable, discord.abc.Connectable, Hashable):
owner_id: :class:`int`
The owner ID that owns the group channel.
.. versionadded:: 2.0
managed: :class:`bool`
Whether the group channel is managed by an application.
This restricts the operations that can be performed on the channel,
and means :attr:`owner` will usually be ``None``.
.. versionadded:: 2.0
application_id: Optional[:class:`int`]
The ID of the managing application, if any.
.. versionadded:: 2.0
name: Optional[:class:`str`]
The group channel's name if provided.
nicks: Dict[:class:`User`, :class:`str`]
A mapping of users to their respective nicknames in the group channel.
.. versionadded:: 2.0
"""
__slots__ = ('last_message_id', 'id', 'recipients', 'owner_id', '_icon', 'name', 'me', '_state', '_accessed')
__slots__ = (
'last_message_id',
'id',
'recipients',
'owner_id',
'managed',
'application_id',
'nicks',
'_icon',
'name',
'me',
'_state',
'_accessed',
)
def __init__(self, *, me: ClientUser, state: ConnectionState, data: GroupChannelPayload):
self._state: ConnectionState = state
@ -2571,11 +2599,14 @@ class GroupChannel(discord.abc.Messageable, discord.abc.Connectable, Hashable):
self._accessed: bool = False
def _update(self, data: GroupChannelPayload) -> None:
self.owner_id: Optional[int] = utils._get_as_snowflake(data, 'owner_id')
self.owner_id: int = int(data['owner_id'])
self._icon: Optional[str] = data.get('icon')
self.name: Optional[str] = data.get('name')
self.recipients: List[User] = [self._state.store_user(u) for u in data.get('recipients', [])]
self.last_message_id: Optional[int] = utils._get_as_snowflake(data, 'last_message_id')
self.managed: bool = data.get('managed', False)
self.application_id: Optional[int] = utils._get_as_snowflake(data, 'application_id')
self.nicks: Dict[User, str] = {utils.get(self.recipients, id=int(k)): v for k, v in data.get('nicks', {}).items()} # type: ignore
def _get_voice_client_key(self) -> Tuple[int, str]:
return self.me.id, 'self_id'
@ -2599,17 +2630,19 @@ class GroupChannel(discord.abc.Messageable, discord.abc.Connectable, Hashable):
if self.name:
return self.name
if len(self.recipients) == 0:
recipients = [x for x in self.recipients if x.id != self.me.id]
if len(recipients) == 0:
return 'Unnamed'
return ', '.join(map(lambda x: x.name, self.recipients))
return ', '.join(map(lambda x: x.name, recipients))
def __repr__(self) -> str:
return f'<GroupChannel id={self.id} name={self.name!r}>'
@property
def notification_settings(self) -> ChannelSettings:
""":class:`~discord.ChannelSettings`: Returns the notification settings for this channel.
""":class:`ChannelSettings`: Returns the notification settings for this channel.
If not found, an instance is created with defaults applied. This follows Discord behaviour.
@ -2621,9 +2654,10 @@ class GroupChannel(discord.abc.Messageable, discord.abc.Connectable, Hashable):
)
@property
def owner(self) -> User:
""":class:`User`: The owner that owns the group channel."""
return utils.find(lambda u: u.id == self.owner_id, self.recipients) # type: ignore # All recipients are always present
def owner(self) -> Optional[User]:
"""Optional[:class:`User`]: The owner that owns the group channel."""
# Only reason it wouldn't be in recipients is if it's a managed channel
return utils.get(self.recipients, id=self.owner_id) or self._state.get_user(self.owner_id)
@property
def call(self) -> Optional[PrivateCall]:
@ -2683,7 +2717,7 @@ class GroupChannel(discord.abc.Messageable, discord.abc.Connectable, Hashable):
Actual direct messages do not really have the concept of permissions.
This returns all the Text related permissions set to ``True`` except:
If a recipient, this returns all the Text related permissions set to ``True`` except:
- :attr:`~Permissions.send_tts_messages`: You cannot send TTS messages in a DM.
- :attr:`~Permissions.manage_messages`: You cannot delete others messages in a DM.
@ -2704,18 +2738,24 @@ class GroupChannel(discord.abc.Messageable, discord.abc.Connectable, Hashable):
:class:`Permissions`
The resolved permissions for the user.
"""
base = Permissions.text()
base.read_messages = True
base.send_tts_messages = False
base.manage_messages = False
base.mention_everyone = True
if obj.id in [x.id for x in self.recipients]:
base = Permissions.text()
base.read_messages = True
base.send_tts_messages = False
base.manage_messages = False
base.mention_everyone = True
if not self.managed:
base.create_instant_invite = True
else:
base = Permissions.none()
if obj.id == self.owner_id:
# Applications can kick members even without being a recipient
base.kick_members = True
return base
async def add_recipients(self, *recipients: Snowflake) -> None:
async def add_recipients(self, *recipients: Snowflake, nicks: Optional[Dict[Snowflake, str]] = None) -> None:
r"""|coro|
Adds recipients to this group.
@ -2729,17 +2769,25 @@ class GroupChannel(discord.abc.Messageable, discord.abc.Connectable, Hashable):
-----------
\*recipients: :class:`~discord.abc.Snowflake`
An argument list of users to add to this group.
If the user is of type :class:`Object`, then the ``nick`` attribute
is used as the nickname for the added recipient.
nicks: Optional[Dict[:class:`~discord.abc.Snowflake`, :class:`str`]]
A mapping of user IDs to nicknames to use for the added recipients.
.. versionadded:: 2.0
Raises
-------
Forbidden
You do not have permissions to add a recipient to this group.
HTTPException
Adding a recipient to this group failed.
"""
# TODO: wait for the corresponding WS event
nicknames = {k.id: v for k, v in nicks.items()} if nicks else {}
await self._get_channel()
req = self._state.http.add_group_recipient
for recipient in recipients:
await req(self.id, recipient.id)
await req(self.id, recipient.id, getattr(recipient, 'nick', (nicknames.get(recipient.id) if nicks else None)))
async def remove_recipients(self, *recipients: Snowflake) -> None:
r"""|coro|
@ -2753,10 +2801,11 @@ class GroupChannel(discord.abc.Messageable, discord.abc.Connectable, Hashable):
Raises
-------
Forbidden
You do not have permissions to remove a recipient from this group.
HTTPException
Removing a recipient from this group failed.
"""
# TODO: wait for the corresponding WS event
await self._get_channel()
req = self._state.http.remove_group_recipient
for recipient in recipients:
@ -2787,7 +2836,7 @@ class GroupChannel(discord.abc.Messageable, discord.abc.Connectable, Hashable):
owner: :class:`~discord.abc.Snowflake`
The new owner of the group.
.. versionadded:: 2.0
.. versionadded:: 2.0
Raises
-------
@ -2804,7 +2853,7 @@ class GroupChannel(discord.abc.Messageable, discord.abc.Connectable, Hashable):
payload['icon'] = None
else:
payload['icon'] = utils._bytes_to_base64_data(icon)
if owner is not MISSING:
if owner:
payload['owner'] = owner.id
data = await self._state.http.edit_channel(self.id, **payload)
@ -2853,11 +2902,36 @@ class GroupChannel(discord.abc.Messageable, discord.abc.Connectable, Hashable):
"""
await self.leave(silent=silent)
async def invites(self) -> List[Invite]:
"""|coro|
Returns a list of all active instant invites from this channel.
.. versionadded:: 2.0
Raises
-------
Forbidden
You do not have proper permissions to get the information.
HTTPException
An error occurred while fetching the information.
Returns
-------
List[:class:`Invite`]
The list of invites that are currently active.
"""
state = self._state
data = await state.http.invites_from_channel(self.id)
return [Invite(state=state, data=invite, channel=self) for invite in data]
async def create_invite(self, *, max_age: int = 86400) -> Invite:
"""|coro|
Creates an instant invite from a group channel.
.. versionadded:: 2.0
Parameters
------------
max_age: :class:`int`
@ -2866,12 +2940,12 @@ class GroupChannel(discord.abc.Messageable, discord.abc.Connectable, Hashable):
Raises
-------
~discord.HTTPException
HTTPException
Invite creation failed.
Returns
--------
:class:`~discord.Invite`
:class:`Invite`
The invite that was created.
"""
data = await self._state.http.create_group_invite(self.id, max_age=max_age)

1454
discord/client.py

File diff suppressed because it is too large

16
discord/commands.py

@ -34,7 +34,7 @@ from .utils import _generate_nonce, _get_as_snowflake
if TYPE_CHECKING:
from .abc import Messageable, Snowflake
from .appinfo import InteractionApplication
from .appinfo import IntegrationApplication
from .file import File
from .guild import Guild
from .interactions import Interaction
@ -80,7 +80,7 @@ class ApplicationCommand(Protocol):
Whether the command is enabled in DMs.
nsfw: :class:`bool`
Whether the command is marked NSFW and only available in NSFW channels.
application: Optional[:class:`~discord.InteractionApplication`]
application: Optional[:class:`~discord.IntegrationApplication`]
The application this command belongs to.
Only available if requested.
application_id: :class:`int`
@ -104,7 +104,7 @@ class ApplicationCommand(Protocol):
dm_permission: bool
nsfw: bool
application_id: int
application: Optional[InteractionApplication]
application: Optional[IntegrationApplication]
mention: str
guild_id: Optional[int]
@ -206,7 +206,7 @@ class BaseCommand(ApplicationCommand, Hashable):
self.type = try_enum(AppCommandType, data['type'])
application = data.get('application')
self.application = state.create_interaction_application(application) if application else None
self.application = state.create_integration_application(application) if application else None
self._default_member_permissions = _get_as_snowflake(data, 'default_member_permissions')
self.default_permission: bool = data.get('default_permission', True)
@ -351,7 +351,7 @@ class UserCommand(BaseCommand):
Whether the command is enabled in DMs.
nsfw: :class:`bool`
Whether the command is marked NSFW and only available in NSFW channels.
application: Optional[:class:`InteractionApplication`]
application: Optional[:class:`IntegrationApplication`]
The application this command belongs to.
Only available if requested.
application_id: :class:`int`
@ -456,7 +456,7 @@ class MessageCommand(BaseCommand):
Whether the command is enabled in DMs.
nsfw: :class:`bool`
Whether the command is marked NSFW and only available in NSFW channels.
application: Optional[:class:`InteractionApplication`]
application: Optional[:class:`IntegrationApplication`]
The application this command belongs to.
Only available if requested.
application_id: :class:`int`
@ -562,7 +562,7 @@ class SlashCommand(BaseCommand, SlashMixin):
Whether the command is enabled in DMs.
nsfw: :class:`bool`
Whether the command is marked NSFW and only available in NSFW channels.
application: Optional[:class:`InteractionApplication`]
application: Optional[:class:`IntegrationApplication`]
The application this command belongs to.
Only available if requested.
application_id: :class:`int`
@ -807,7 +807,7 @@ class SubCommand(SlashMixin):
@property
def application(self):
"""Optional[:class:`InteractionApplication`]: The application this command belongs to.
"""Optional[:class:`IntegrationApplication`]: The application this command belongs to.
Only available if requested.
"""
return self._parent.application

79
discord/connections.py

@ -23,10 +23,11 @@ DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Iterator, List, Optional, Tuple
from typing import TYPE_CHECKING, List, Optional
from .enums import ConnectionType, try_enum
from .integrations import Integration
from .metadata import Metadata
from .utils import MISSING
if TYPE_CHECKING:
@ -38,7 +39,6 @@ if TYPE_CHECKING:
__all__ = (
'PartialConnection',
'Connection',
'ConnectionMetadata',
)
@ -79,9 +79,13 @@ class PartialConnection:
Whether the connection is verified.
visible: :class:`bool`
Whether the connection is visible on the user's profile.
metadata: Optional[:class:`Metadata`]
Various metadata about the connection.
The contents of this are always subject to change.
"""
__slots__ = ('id', 'name', 'type', 'verified', 'visible')
__slots__ = ('id', 'name', 'type', 'verified', 'visible', 'metadata')
def __init__(self, data: PartialConnectionPayload):
self._update(data)
@ -112,6 +116,8 @@ class PartialConnection:
self.verified: bool = data['verified']
self.visible: bool = True # If we have a partial connection, it's visible
self.metadata: Optional[Metadata] = Metadata(data['metadata']) if 'metadata' in data else None
@property
def url(self) -> Optional[str]:
"""Optional[:class:`str`]: Returns a URL linking to the connection's profile, if available."""
@ -174,7 +180,7 @@ class Connection(PartialConnection):
Whether the connection is authorized both ways (i.e. it's both a connection and an authorization).
metadata_visible: :class:`bool`
Whether the connection's metadata is visible.
metadata: Optional[:class:`ConnectionMetadata`]
metadata: Optional[:class:`Metadata`]
Various metadata about the connection.
The contents of this are always subject to change.
@ -191,7 +197,6 @@ class Connection(PartialConnection):
'show_activity',
'two_way_link',
'metadata_visible',
'metadata',
'access_token',
'integrations',
)
@ -209,7 +214,6 @@ class Connection(PartialConnection):
self.show_activity: bool = data.get('show_activity', True)
self.two_way_link: bool = data.get('two_way_link', False)
self.metadata_visible: bool = bool(data.get('metadata_visibility', False))
self.metadata: Optional[ConnectionMetadata] = ConnectionMetadata(data['metadata']) if 'metadata' in data else None
# Only sometimes in the payload
try:
@ -328,66 +332,3 @@ class Connection(PartialConnection):
"""
data = await self._state.http.get_connection_token(self.type.value, self.id)
return data['access_token']
class ConnectionMetadata:
"""Represents a connection's metadata.
Because of how unstable and wildly varying this metadata can be, this is a simple class that just
provides access ro the raw data using dot notation. This means if an attribute is not present,
``None`` will be returned instead of raising an AttributeError.
.. versionadded:: 2.0
.. container:: operations
.. describe:: x == y
Checks if two metadata objects are equal.
.. describe:: x != y
Checks if two metadata objects are not equal.
.. describe:: x[key]
Returns a metadata value if it is found, otherwise raises a :exc:`KeyError`.
.. describe:: key in x
Checks if a metadata value is present.
.. describe:: iter(x)
Returns an iterator of ``(field, value)`` pairs. This allows this class
to be used as an iterable in list/dict/etc constructions.
"""
__slots__ = ()
def __init__(self, data: Optional[dict]) -> None:
self.__dict__.update(data or {})
def __repr__(self) -> str:
return f'<ConnectionMetadata {" ".join(f"{k}={v!r}" for k, v in self.__dict__.items())}>'
def __eq__(self, other: object) -> bool:
if not isinstance(other, ConnectionMetadata):
return False
return self.__dict__ == other.__dict__
def __ne__(self, other: object) -> bool:
if not isinstance(other, ConnectionMetadata):
return True
return self.__dict__ != other.__dict__
def __iter__(self) -> Iterator[Tuple[str, Any]]:
yield from self.__dict__.items()
def __getitem__(self, key: str) -> Any:
return self.__dict__[key]
def __getattr__(self, attr: str) -> Any:
return None
def __contains__(self, key: str) -> bool:
return key in self.__dict__

632
discord/entitlements.py

@ -0,0 +1,632 @@
"""
The MIT License (MIT)
Copyright (c) 2021-present Dolfies
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, List, Optional
from .enums import EntitlementType, GiftStyle, PremiumType, try_enum
from .flags import GiftFlags
from .mixins import Hashable
from .payments import EntitlementPayment
from .promotions import Promotion
from .store import SKU, StoreListing, SubscriptionPlan
from .subscriptions import Subscription, SubscriptionTrial
from .utils import _get_as_snowflake, parse_time, utcnow
if TYPE_CHECKING:
from datetime import datetime
from .abc import Snowflake
from .guild import Guild
from .state import ConnectionState
from .types.entitlements import (
Entitlement as EntitlementPayload,
Gift as GiftPayload,
GiftBatch as GiftBatchPayload,
)
from .user import User
__all__ = (
'Entitlement',
'Gift',
'GiftBatch',
)
class Entitlement(Hashable):
"""Represents a Discord entitlement.
.. container:: operations
.. describe:: x == y
Checks if two entitlements are equal.
.. describe:: x != y
Checks if two entitlements are not equal.
.. describe:: hash(x)
Returns the entitlement's hash.
.. describe:: bool(x)
Checks if the entitlement is active.
.. versionadded:: 2.0
Attributes
----------
id: :class:`int`
The ID of the entitlement.
type: :class:`EntitlementType`
The type of entitlement.
user_id: :class:`int`
The ID of the user the entitlement is for.
sku_id: :class:`int`
The ID of the SKU the entitlement grants.
application_id: :class:`int`
The ID of the application that owns the SKU the entitlement grants.
promotion_id: Optional[:class:`int`]
The ID of the promotion the entitlement is from.
parent_id: Optional[:class:`int`]
The ID of the entitlement's parent.
guild_id: Optional[:class:`int`]
The ID of the guild the entitlement is for.
branches: List[:class:`int`]
The IDs of the branches the entitlement grants.
gifter_id: Optional[:class:`int`]
The ID of the user that gifted the entitlement.
gift_style: Optional[:class:`GiftStyle`]
The style of the gift attached to this entitlement.
gift_batch_id: Optional[:class:`int`]
The ID of the batch the gift attached to this entitlement is from.
deleted: :class:`bool`
Whether the entitlement is deleted.
consumed: :class:`bool`
Whether the entitlement is consumed.
starts_at: Optional[:class:`datetime.datetime`]
When the entitlement period starts.
ends_at: Optional[:class:`datetime.datetime`]
When the entitlement period ends.
subscription_id: Optional[:class:`int`]
The ID of the subscription the entitlement is from.
subscription_plan: Optional[:class:`SubscriptionPlan`]
The subscription plan the entitlement is for.
.. note::
This is a partial object without price information.
sku: Optional[:class:`SKU`]
The SKU the entitlement grants.
payment: Optional[:class:`EntitlementPayment`]
The payment made for the entitlement.
Not available in some contexts.
"""
__slots__ = (
'id',
'type',
'user_id',
'sku_id',
'application_id',
'promotion_id',
'parent_id',
'guild_id',
'branches',
'gifter_id',
'gift_style',
'gift_batch_id',
'_gift_flags',
'deleted',
'consumed',
'starts_at',
'ends_at',
'subscription_id',
'subscription_plan',
'sku',
'payment',
'_state',
)
def __init__(self, *, data: EntitlementPayload, state: ConnectionState):
self._state = state
self._update(data)
def _update(self, data: EntitlementPayload):
state = self._state
self.id: int = int(data['id'])
self.type: EntitlementType = try_enum(EntitlementType, data['type'])
self.user_id: int = int(data.get('user_id') or state.self_id) # type: ignore
self.sku_id: int = int(data['sku_id'])
self.application_id: int = int(data['application_id'])
self.promotion_id: Optional[int] = _get_as_snowflake(data, 'promotion_id')
self.parent_id: Optional[int] = _get_as_snowflake(data, 'parent_id')
self.guild_id: Optional[int] = _get_as_snowflake(data, 'guild_id')
self.branches: List[int] = [int(branch) for branch in data.get('branches', [])]
self.gifter_id: Optional[int] = _get_as_snowflake(data, 'gifter_user_id')
self.gift_style: Optional[GiftStyle] = try_enum(GiftStyle, data.get('gift_style'))
self.gift_batch_id: Optional[int] = _get_as_snowflake(data, 'gift_code_batch_id')
self._gift_flags: int = data.get('gift_code_flags', 0)
self.deleted: bool = data.get('deleted', False)
self.consumed: bool = data.get('consumed', False)
self.starts_at: Optional[datetime] = parse_time(data.get('starts_at'))
self.ends_at: Optional[datetime] = parse_time(data.get('ends_at'))
self.subscription_id: Optional[int] = _get_as_snowflake(data, 'subscription_id')
self.subscription_plan: Optional[SubscriptionPlan] = (
SubscriptionPlan(data=data['subscription_plan'], state=state) if 'subscription_plan' in data else None
)
self.sku: Optional[SKU] = SKU(data=data['sku'], state=state) if 'sku' in data else None
self.payment: Optional[EntitlementPayment] = (
EntitlementPayment(data=data['payment'], entitlement=self) if 'payment' in data else None
)
def __repr__(self) -> str:
return f'<Entitlement id={self.id} type={self.type!r} sku_id={self.sku_id} application_id={self.application_id}>'
def __bool__(self) -> bool:
return self.is_active()
@property
def guild(self) -> Optional[Guild]:
""":class:`Guild`: Returns the guild the entitlement is for, if accessible."""
return self._state._get_guild(self.guild_id)
@property
def premium_type(self) -> Optional[PremiumType]:
"""Optional[:class:`PremiumType`]: The premium type this entitlement grants, if it is for a premium subscription."""
return PremiumType.from_sku_id(self.sku_id)
@property
def gift_flags(self) -> GiftFlags:
""":class:`GiftFlags`: Returns the flags for the gift this entitlement is attached to."""
return GiftFlags._from_value(self._gift_flags)
def is_giftable(self) -> bool:
""":class:`bool`: Whether the entitlement is giftable."""
return self.type == EntitlementType.user_gift and not self.gifter_id
def is_active(self) -> bool:
""":class:`bool`: Whether the entitlement is active and offering perks."""
# This is a copy of the logic used in the client
if self.is_giftable() or self.deleted:
return False # Giftable entitlements have not yet been gifted therefore are not active
if self.starts_at and self.starts_at > utcnow():
return False # Entitlement has not started yet
if self.ends_at and self.ends_at < utcnow():
return False # Entitlement has ended
if self.type == EntitlementType.premium_subscription:
# Premium subscription entitlements are only active
# if the SKU is offered for free to premium subscribers
# and the user is a premium subscriber
sku = self.sku
if sku and not sku.premium:
return False
if self._state.user and not self._state.user.premium_type == PremiumType.nitro:
return False
return True
async def subscription(self) -> Optional[Subscription]:
"""|coro|
Retrieves the subscription this entitlement is attached to, if applicable.
Raises
------
NotFound
You cannot access this subscription.
HTTPException
Fetching the subscription failed.
Returns
-------
Optional[:class:`Subscription`]
The retrieved subscription, if applicable.
"""
if not self.subscription_id:
return
data = await self._state.http.get_subscription(self.subscription_id)
return Subscription(data=data, state=self._state)
async def consume(self) -> None:
"""|coro|
Consumes the entitlement. This marks a given user entitlement as expended,
and removes the entitlement from the user's active entitlements.
This should be called after the user has received the relevant item,
and only works on entitlements for SKUs of type :attr:`SKUType.consumable`.
Raises
------
Forbidden
You do not have permissions to access this application.
HTTPException
Consuming the entitlement failed.
"""
await self._state.http.consume_app_entitlement(self.application_id, self.id)
async def delete(self) -> None:
"""|coro|
Deletes the entitlement. This removes the entitlement from the user's
entitlements, and is irreversible.
This is only useable on entitlements of type :attr:`EntitlementType.test_mode_purchase`.
Raises
------
Forbidden
You do not have permissions to access this application.
HTTPException
Deleting the entitlement failed.
"""
await self._state.http.delete_app_entitlement(self.application_id, self.id)
class Gift:
"""Represents a Discord gift.
.. container:: operations
.. describe:: x == y
Checks if two gifts are equal.
.. describe:: x != y
Checks if two gifts are not equal.
.. describe:: hash(x)
Returns the gift's hash.
.. versionadded:: 2.0
Attributes
----------
code: :class:`str`
The gift's code.
expires_at: Optional[:class:`datetime.datetime`]
When the gift expires.
application_id: Optional[:class:`int`]
The ID of the application that owns the SKU the gift is for.
Not available in all contexts.
batch_id: Optional[:class:`int`]
The ID of the batch the gift is from.
sku_id: :class:`int`
The ID of the SKU the gift is for.
entitlement_branches: List[:class:`int`]
A list of entitlements the gift is for.
gift_style: Optional[:class:`GiftStyle`]
The style of the gift.
max_uses: :class:`int`
The maximum number of times the gift can be used.
uses: :class:`int`
The number of times the gift has been used.
redeemed: :class:`bool`
Whether the user has redeemed the gift.
revoked: :class:`bool`
Whether the gift has been revoked.
guild_id: Optional[:class:`int`]
The ID of the guild the gift was redeemed in.
Not available in all contexts.
channel_id: Optional[:class:`int`]
The ID of the channel the gift was redeemed in.
Not available in all contexts.
store_listing: Optional[:class:`StoreListing`]
The store listing for the SKU the gift is for.
Not available in all contexts.
promotion: Optional[:class:`Promotion`]
The promotion the gift is a part of, if any.
subscription_trial: Optional[:class:`SubscriptionTrial`]
The subscription trial the gift is a part of, if any.
subscription_plan_id: Optional[:class:`int`]
The ID of the subscription plan the gift is for, if any.
subscription_plan: Optional[:class:`SubscriptionPlan`]
The subscription plan the gift is for, if any.
user: Optional[:class:`User`]
The user who created the gift, if applicable.
"""
__slots__ = (
'code',
'expires_at',
'application_id',
'batch_id',
'sku_id',
'entitlement_branches',
'gift_style',
'_flags',
'max_uses',
'uses',
'redeemed',
'revoked',
'guild_id',
'channel_id',
'store_listing',
'promotion',
'subscription_trial',
'subscription_plan_id',
'subscription_plan',
'user',
'_state',
)
def __init__(self, *, data: GiftPayload, state: ConnectionState) -> None:
self._state = state
self._update(data)
def _update(self, data: GiftPayload) -> None:
state = self._state
self.code: str = data['code']
self.expires_at: Optional[datetime] = parse_time(data.get('expires_at'))
self.application_id: Optional[int] = _get_as_snowflake(data, 'application_id')
self.batch_id: Optional[int] = _get_as_snowflake(data, 'batch_id')
self.subscription_plan_id: Optional[int] = _get_as_snowflake(data, 'subscription_plan_id')
self.sku_id: int = int(data['sku_id'])
self.entitlement_branches: List[int] = [int(x) for x in data.get('entitlement_branches', [])]
self.gift_style: Optional[GiftStyle] = try_enum(GiftStyle, data['gift_style']) if data.get('gift_style') else None # type: ignore
self._flags: int = data.get('flags', 0)
self.max_uses: int = data.get('max_uses', 0)
self.uses: int = data.get('uses', 0)
self.redeemed: bool = data.get('redeemed', False)
self.revoked: bool = data.get('revoked', False)
self.guild_id: Optional[int] = _get_as_snowflake(data, 'guild_id')
self.channel_id: Optional[int] = _get_as_snowflake(data, 'channel_id')
self.store_listing: Optional[StoreListing] = (
StoreListing(data=data['store_listing'], state=state) if 'store_listing' in data else None
)
self.promotion: Optional[Promotion] = Promotion(data=data['promotion'], state=state) if 'promotion' in data else None
self.subscription_trial: Optional[SubscriptionTrial] = (
SubscriptionTrial(data['subscription_trial']) if 'subscription_trial' in data else None
)
self.subscription_plan: Optional[SubscriptionPlan] = (
SubscriptionPlan(data=data['subscription_plan'], state=state) if 'subscription_plan' in data else None
)
self.user: Optional[User] = self._state.create_user(data['user']) if 'user' in data else None
def __repr__(self) -> str:
return f'<Gift code={self.code!r} sku_id={self.sku_id} uses={self.uses} max_uses={self.max_uses} redeemed={self.redeemed}>'
def __eq__(self, other: Any) -> bool:
return isinstance(other, Gift) and other.code == self.code
def __ne__(self, other: Any) -> bool:
if isinstance(other, Gift):
return other.code != self.code
return True
def __hash__(self) -> int:
return hash(self.code)
@property
def id(self) -> str:
""":class:`str`: Returns the code portion of the gift."""
return self.code
@property
def url(self) -> str:
""":class:`str`: Returns the gift's URL."""
return f'https://discord.gift/{self.code}'
@property
def remaining_uses(self) -> int:
""":class:`int`: Returns the number of remaining uses for the gift."""
return self.max_uses - self.uses
@property
def flags(self) -> GiftFlags:
""":class:`GiftFlags`: Returns the gift's flags."""
return GiftFlags._from_value(self._flags)
@property
def premium_type(self) -> Optional[PremiumType]:
"""Optional[:class:`PremiumType`]: The premium type this gift grants, if it is for a premium subscription."""
return PremiumType.from_sku_id(self.sku_id) if self.is_subscription() else None
def is_claimed(self) -> bool:
""":class:`bool`: Checks if the gift has been used up."""
return self.uses >= self.max_uses if self.max_uses else False
def is_expired(self) -> bool:
""":class:`bool`: Checks if the gift has expired."""
return self.expires_at < utcnow() if self.expires_at else False
def is_subscription(self) -> bool:
""":class:`bool`: Checks if the gift is for a subscription."""
return self.subscription_plan_id is not None
def is_premium_subscription(self) -> bool:
""":class:`bool`: Checks if the gift is for a premium subscription."""
return self.is_subscription() and self.application_id == self._state.premium_subscriptions_application.id
async def redeem(
self,
payment_source: Optional[Snowflake] = None,
*,
channel: Optional[Snowflake] = None,
gateway_checkout_context: Optional[str] = None,
) -> Entitlement:
"""|coro|
Redeems the gift.
Parameters
----------
payment_source: Optional[:class:`PaymentSource`]
The payment source to use for the redemption.
Only required if the gift's :attr:`flags` have :attr:`GiftFlags.payment_source_required` set to ``True``.
channel: Optional[Union[:class:`TextChannel`, :class:`VoiceChannel`, :class:`Thread`, :class:`DMChannel`, :class:`GroupChannel`]]
The channel to redeem the gift in. This is usually the channel the gift was sent in.
While this is optional, it is recommended to pass this in.
gateway_checkout_context: Optional[:class:`str`]
The current checkout context.
Raises
------
HTTPException
The gift failed to redeem.
Returns
-------
:class:`Entitlement`
The entitlement that was created from redeeming the gift.
"""
data = await self._state.http.redeem_gift(
self.code,
payment_source.id if payment_source else None,
channel.id if channel else None,
gateway_checkout_context,
)
return Entitlement(data=data, state=self._state)
async def delete(self) -> None:
"""|coro|
Revokes the gift.
This is only possible for gifts the current account has created.
Raises
------
NotFound
The owned gift was not found.
HTTPException
The gift failed to delete.
"""
await self._state.http.delete_gift(self.code)
class GiftBatch(Hashable):
"""Represents a batch of gifts for an SKU.
.. container:: operations
.. describe:: x == y
Checks if two gift batches are equal.
.. describe:: x != y
Checks if two gift batches are not equal.
.. describe:: hash(x)
Returns the gift batch's hash.
.. describe:: str(x)
Returns the gift batch's description.
.. versionadded:: 2.0
Attributes
-----------
id: :class:`int`
The ID of the gift batch.
application_id: :class:`int`
The ID of the application the gift batch is for.
sku_id: :class:`int`
The ID of the SKU the gift batch is for.
amount: :class:`int`
The amount of gifts in the batch.
description: :class:`str`
The description of the gift batch.
entitlement_branches: List[:class:`int`]
The entitlement branches the gift batch is for.
entitlement_starts_at: Optional[:class:`datetime.datetime`]
When the entitlement is valid from.
entitlement_ends_at: Optional[:class:`datetime.datetime`]
When the entitlement is valid until.
"""
__slots__ = (
'id',
'application_id',
'sku_id',
'amount',
'description',
'entitlement_branches',
'entitlement_starts_at',
'entitlement_ends_at',
'_state',
)
def __init__(self, *, data: GiftBatchPayload, state: ConnectionState, application_id: int) -> None:
self._state: ConnectionState = state
self.id: int = int(data['id'])
self.application_id = application_id
self.sku_id: int = int(data['sku_id'])
self.amount: int = data['amount']
self.description: str = data.get('description', '')
self.entitlement_branches: List[int] = [int(branch) for branch in data.get('entitlement_branches', [])]
self.entitlement_starts_at: Optional[datetime] = parse_time(data.get('entitlement_starts_at'))
self.entitlement_ends_at: Optional[datetime] = parse_time(data.get('entitlement_ends_at'))
def __repr__(self) -> str:
return f'<GiftBatch id={self.id} sku_id={self.sku_id} amount={self.amount} description={self.description!r}>'
def __str__(self) -> str:
return self.description
def is_valid(self) -> bool:
""":class:`bool`: Checks if the gift batch is valid."""
if self.entitlement_starts_at and self.entitlement_starts_at > utcnow():
return False
if self.entitlement_ends_at and self.entitlement_ends_at < utcnow():
return False
return True
async def download(self) -> bytes:
"""|coro|
Returns the gifts in the gift batch in CSV format.
Raises
-------
Forbidden
You do not have permissions to download the batch.
HTTPException
Downloading the batch failed.
Returns
-------
:class:`bytes`
The report content.
"""
return await self._state.http.get_gift_batch_csv(self.application_id, self.id)

472
discord/enums.py

@ -42,7 +42,10 @@ __all__ = (
'ActivityType',
'NotificationLevel',
'HighlightLevel',
'TeamMembershipState',
'ApplicationMembershipState',
'PayoutAccountStatus',
'PayoutStatus',
'PayoutReportType',
'WebhookType',
'ExpireBehaviour',
'ExpireBehavior',
@ -53,6 +56,7 @@ __all__ = (
'ComponentType',
'ButtonStyle',
'TextStyle',
'GiftStyle',
'PrivacyLevel',
'InteractionType',
'NSFWLevel',
@ -76,12 +80,33 @@ __all__ = (
'ApplicationVerificationState',
'StoreApplicationState',
'RPCApplicationState',
'ApplicationDiscoverabilityState',
'InviteType',
'ScheduledEventStatus',
'ScheduledEventEntityType',
'ApplicationType',
'EmbeddedActivityPlatform',
'EmbeddedActivityOrientation',
'ConnectionType',
'ConnectionLinkType',
'PaymentSourceType',
'PaymentGateway',
'SubscriptionType',
'SubscriptionStatus',
'SubscriptionInvoiceStatus',
'SubscriptionDiscountType',
'SubscriptionInterval',
'SubscriptionPlanPurchaseType',
'PaymentStatus',
'ApplicationAssetType',
'SKUType',
'SKUAccessLevel',
'SKUFeature',
'SKUGenre',
'OperatingSystem',
'ContentRatingAgency',
'Distributor',
'EntitlementType',
)
if TYPE_CHECKING:
@ -564,6 +589,7 @@ class UserFlags(Enum):
bot_http_interactions = 524288
spammer = 1048576
disable_premium = 2097152
quarantined = 17592186044416
class ActivityType(Enum):
@ -585,16 +611,63 @@ class HypeSquadHouse(Enum):
balance = 3
class PremiumType(Enum, comparable=True):
class PremiumType(Enum):
none = 0
nitro_classic = 1
nitro = 2
nitro_basic = 3
class TeamMembershipState(Enum, comparable=True):
@classmethod
def from_sku_id(cls, sku_id: int) -> Optional[PremiumType]:
if sku_id == 628379670982688768:
return cls.none
elif sku_id == 521846918637420545:
return cls.nitro_classic
elif sku_id in (521842865731534868, 521847234246082599):
return cls.nitro
elif sku_id == 978380684370378762:
return cls.nitro_basic
class ApplicationMembershipState(Enum, comparable=True):
invited = 1
accepted = 2
class PayoutAccountStatus(Enum):
unsubmitted = 1
pending = 2
action_required = 3
active = 4
blocked = 5
suspended = 6
class PayoutStatus(Enum):
open = 1
paid = 2
pending = 3
manual = 4
canceled = 5
cancelled = 5
deferred = 6
deferred_internal = 7
processing = 8
error = 9
rejected = 10
risk_review = 11
submitted = 12
pending_funds = 13
class PayoutReportType(Enum):
by_sku = 'sku'
by_transaction = 'transaction'
def __str__(self) -> str:
return self.value
class WebhookType(Enum):
incoming = 1
channel_follower = 2
@ -605,6 +678,9 @@ class ExpireBehaviour(Enum):
remove_role = 0
kick = 1
def __int__(self) -> int:
return self.value
ExpireBehavior = ExpireBehaviour
@ -661,7 +737,9 @@ class RequiredActionType(Enum):
verify_phone = 'REQUIRE_VERIFIED_PHONE'
verify_email = 'REQUIRE_VERIFIED_EMAIL'
complete_captcha = 'REQUIRE_CAPTCHA'
accept_terms = 'AGREEMENTS'
update_agreements = 'AGREEMENTS'
acknowledge_tos_update = 'TOS_UPDATE_ACKNOWLEDGMENT'
none = None
class InviteTarget(Enum):
@ -735,6 +813,15 @@ class TextStyle(Enum):
return self.value
class GiftStyle(Enum):
snowglobe = 1
box = 2
cup = 3
def __int__(self) -> int:
return self.value
class PrivacyLevel(Enum):
public = 1
closed = 2
@ -791,13 +878,54 @@ class RPCApplicationState(Enum, comparable=True):
rejected = 4
class ApplicationDiscoverabilityState(Enum, comparable=True):
ineligible = 1
not_discoverable = 2
discoverable = 3
featureable = 4
blocked = 5
class ApplicationBuildStatus(Enum):
created = 'CREATED'
uploading = 'UPLOADING'
uploaded = 'UPLOADED'
invalid = 'INVALID'
validating = 'VALIDATING'
corrupted = 'CORRUPTED'
ready = 'READY'
def __str__(self) -> str:
return self.value
class ApplicationType(Enum):
none = None
game = 1
music = 2
ticketed_events = 3
guild_role_subscriptions = 4
def __int__(self) -> int:
return self.value
class EmbeddedActivityPlatform(Enum):
web = 'web'
ios = 'ios'
android = 'android'
def __str__(self) -> str:
return self.value
class EmbeddedActivityOrientation(Enum):
unlocked = 1
portrait = 2
landscape = 3
def __int__(self) -> int:
return self.value
T = TypeVar('T')
@ -918,6 +1046,338 @@ class ConnectionLinkType(Enum):
return self.value
class PaymentSourceType(Enum):
unknown = 0
card = 1
paypal = 2
giropay = 3
sofort = 4
przzelewy24 = 5
sepa_debit = 6
paysafecard = 7
gcash = 8
grabpay = 9
momo_wallet = 10
venmo = 11
gopay_wallet = 12
kakaopay = 13
bancontact = 14
eps = 15
ideal = 16
payment_request = 99
class PaymentGateway(Enum):
stripe = 1
braintree = 2
apple = 3
google = 4
adyen = 5
apple_pay = 6
def __int__(self) -> int:
return self.value
class SubscriptionType(Enum):
premium = 1
guild = 2
application = 3
class SubscriptionStatus(Enum):
unpaid = 0
active = 1
past_due = 2
canceled = 3
cancelled = 3
ended = 4
inactive = 5
account_hold = 6
def __int__(self) -> int:
return self.value
class SubscriptionInvoiceStatus(Enum, comparable=True):
open = 1
paid = 2
void = 3
uncollectible = 4
class SubscriptionDiscountType(Enum):
subscription_plan = 1
entitlement = 2
premium_legacy_upgrade_promotion = 3
premium_trial = 4
class SubscriptionInterval(Enum):
month = 1
year = 2
day = 3
class SubscriptionPlanPurchaseType(Enum):
default = 0
gift = 1
sale = 2
nitro_classic = 3
nitro = 4
class PaymentStatus(Enum):
pending = 0
completed = 1
failed = 2
reversed = 3
refunded = 4
canceled = 5
cancelled = 5
class ApplicationAssetType(Enum):
one = 1
two = 2
def __int__(self) -> int:
return self.value
class SKUType(Enum):
durable_primary = 1
durable = 2
consumable = 3
bundle = 4
subscription = 5
group = 6
def __int__(self) -> int:
return self.value
class SKUAccessLevel(Enum, comparable=True):
full = 1
early_access = 2
vip_access = 3
def __int__(self) -> int:
return self.value
class SKUFeature(Enum):
single_player = 1
online_multiplayer = 2
local_multiplayer = 3
pvp = 4
local_coop = 5
cross_platform = 6
rich_presence = 7
discord_game_invites = 8
spectator_mode = 9
controller_support = 10
cloud_saves = 11
online_coop = 12
secure_networking = 13
def __int__(self) -> int:
return self.value
class SKUGenre(Enum):
action = 1
action_adventure = 9
action_rpg = 2
adventure = 8
artillery = 50
baseball = 34
basketball = 35
billiards = 36
bowling = 37
boxing = 38
brawler = 3
card_game = 58
driving_racing = 16
dual_joystick_shooter = 27
dungeon_crawler = 21
education = 59
fighting = 56
fishing = 32
fitness = 60
flight_simulator = 29
football = 39
four_x = 49
fps = 26
gambling = 61
golf = 40
hack_and_slash = 4
hockey = 41
life_simulator = 31
light_gun = 24
massively_multiplayer = 18
metroidvania = 10
mmorpg = 19
moba = 55
music_rhythm = 62
open_world = 11
party_mini_game = 63
pinball = 64
platformer = 5
psychological_horror = 12
puzzle = 57
rpg = 22
role_playing = 20
rts = 51
sandbox = 13
shooter = 23
shoot_em_up = 25
simulation = 28
skateboarding_skating = 42
snowboarding_skiing = 43
soccer = 44
sports = 33
stealth = 6
strategy = 48
surfing_wakeboarding = 46
survival = 7
survival_horror = 14
tower_defense = 52
track_field = 45
train_simulator = 30
trivia_board_game = 65
turn_based_strategy = 53
vehicular_combat = 17
visual_novel = 15
wargame = 54
wrestling = 47
def __int__(self) -> int:
return self.value
class OperatingSystem(Enum):
windows = 1
mac = 2
linux = 3
class ContentRatingAgency(Enum):
esrb = 1
pegi = 2
class ESRBRating(Enum):
everyone = 1
everyone_ten_plus = 2
teen = 3
mature = 4
adults_only = 5
rating_pending = 6
def __int__(self) -> int:
return self.value
class PEGIRating(Enum):
three = 1
seven = 2
twelve = 3
sixteen = 4
eighteen = 5
def __int__(self) -> int:
return self.value
class ESRBContentDescriptor(Enum):
alcohol_reference = 1
animated_blood = 2
blood = 3
blood_and_gore = 4
cartoon_violence = 5
comic_mischief = 6
crude_humor = 7
drug_reference = 8
fantasy_violence = 9
intense_violence = 10
language = 11
lyrics = 12
mature_humor = 13
nudity = 14
partial_nudity = 15
real_gambling = 16
sexual_content = 17
sexual_themes = 18
sexual_violence = 19
simulated_gambling = 20
strong_language = 21
strong_lyrics = 22
strong_sexual_content = 23
suggestive_themes = 24
tobacco_reference = 25
use_of_alcohol = 26
use_of_drugs = 27
use_of_tobacco = 28
violence = 29
violent_references = 30
in_game_purchases = 31
users_interact = 32
shares_location = 33
unrestricted_internet = 34
mild_blood = 35
mild_cartoon_violence = 36
mild_fantasy_violence = 37
mild_language = 38
mild_lyrics = 39
mild_sexual_themes = 40
mild_suggestive_themes = 41
mild_violence = 42
animated_violence = 43
def __int__(self) -> int:
return self.value
class PEGIContentDescriptor(Enum):
violence = 1
bad_language = 2
fear = 3
gambling = 4
sex = 5
drugs = 6
discrimination = 7
def __int__(self) -> int:
return self.value
class Distributor(Enum):
discord = 'discord'
steam = 'steam'
twitch = 'twitch'
uplay = 'uplay'
battle_net = 'battlenet'
origin = 'origin'
gog = 'gog'
epic_games = 'epic'
google_play = 'google_play'
class EntitlementType(Enum):
purchase = 1
premium_subscription = 2
developer_gift = 3
test_mode_purchase = 4
free_purchase = 5
user_gift = 6
premium_purchase = 7
application_subscription = 8
def __int__(self) -> int:
return self.value
def create_unknown_value(cls: Type[E], val: Any) -> E:
value_cls = cls._enum_value_cls_ # type: ignore # This is narrowed below
name = f'unknown_{val}'

18
discord/errors.py

@ -25,6 +25,8 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
from typing import Dict, List, Optional, TYPE_CHECKING, Any, Tuple, Union
from .utils import _get_as_snowflake
if TYPE_CHECKING:
from aiohttp import ClientResponse, ClientWebSocketResponse
@ -79,6 +81,11 @@ class GatewayNotFound(DiscordException):
def _flatten_error_dict(d: Dict[str, Any], key: str = '') -> Dict[str, str]:
items: List[Tuple[str, str]] = []
if '_errors' in d:
items.append(('miscallenous', ' '.join(x.get('message', '') for x in d['_errors'])))
d.pop('_errors')
for k, v in d.items():
new_key = key + '.' + k if key else k
@ -112,6 +119,12 @@ class HTTPException(DiscordException):
The Discord specific error code for the failure.
json: :class:`dict`
The raw error JSON.
.. versionadded:: 2.0
payment_id: Optional[:class:`int`]
The ID of the payment that requires verification to continue.
.. versionadded:: 2.0
"""
def __init__(self, response: _ResponseType, message: Optional[Union[str, Dict[str, Any]]]):
@ -119,6 +132,8 @@ class HTTPException(DiscordException):
self.status: int = response.status # type: ignore # This attribute is filled by the library even if using requests
self.code: int
self.text: str
self.json: Dict[str, Any]
self.payment_id: Optional[int]
if isinstance(message, dict):
self.json = message
self.code = message.get('code', 0)
@ -130,9 +145,12 @@ class HTTPException(DiscordException):
self.text = base + '\n' + helpful
else:
self.text = base
self.payment_id = _get_as_snowflake(message, 'payment_id')
else:
self.text = message or ''
self.code = 0
self.json = {}
self.payment_id = None
fmt = '{0.status} {0.reason} (error code: {1})'
if len(self.text):

17
discord/file.py

@ -23,12 +23,14 @@ DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import Any, Dict, Optional, Tuple, Union
import os
from base64 import b64encode
from hashlib import md5
import io
import os
from typing import Any, Dict, Optional, Tuple, Union
from .utils import MISSING
from .utils import MISSING, cached_slot_property
# fmt: off
__all__ = (
@ -77,7 +79,7 @@ class File:
.. versionadded:: 2.0
"""
__slots__ = ('fp', '_filename', 'spoiler', 'description', '_original_pos', '_owner', '_closer')
__slots__ = ('fp', '_filename', 'spoiler', 'description', '_original_pos', '_owner', '_closer', '_cs_md5')
def __init__(
self,
@ -129,6 +131,13 @@ class File:
def filename(self, value: str) -> None:
self._filename, self.spoiler = _strip_spoiler(value)
@cached_slot_property('_cs_md5')
def md5(self) -> str:
try:
return b64encode(md5(self.fp.read()).digest()).decode('utf-8')
finally:
self.reset()
def reset(self, *, seek: Union[int, bool] = True) -> None:
# The `seek` parameter is needed because
# the retry-loop is iterated over multiple times

610
discord/flags.py

@ -42,6 +42,13 @@ __all__ = (
'ChannelFlags',
'PremiumUsageFlags',
'PurchasedFlags',
'PaymentSourceFlags',
'SKUFlags',
'PaymentFlags',
'PromotionFlags',
'GiftFlags',
'LibraryApplicationFlags',
'ApplicationDiscoveryFlags',
)
BF = TypeVar('BF', bound='BaseFlags')
@ -379,17 +386,14 @@ class PublicUserFlags(BaseFlags):
@flag_value
def bug_hunter(self):
""":class:`bool`: Returns ``True`` if the user is a level 1 Bug Hunter
There is an alias for this called :attr:`bug_hunter_level_1`.
"""
""":class:`bool`: Returns ``True`` if the user is a level 1 Bug Hunter."""
return UserFlags.bug_hunter.value
@alias_flag_value
def bug_hunter_level_1(self):
""":class:`bool`: Returns ``True`` if the user is a Bug Hunter
""":class:`bool`: An alias for :attr:`bug_hunter`.
This is an alias of :attr:`bug_hunter`.
.. versionadded:: 2.0
"""
return UserFlags.bug_hunter_level_1.value
@ -495,7 +499,7 @@ class PrivateUserFlags(PublicUserFlags):
Returns an iterator of ``(name, value)`` pairs. This allows it
to be, for example, constructed as a dict or a list of pairs.
Note that aliases are not shown.
Note that aliases or inherited flags are not shown.
.. note::
These are only available on your own user flags.
@ -514,7 +518,7 @@ class PrivateUserFlags(PublicUserFlags):
@flag_value
def premium_promo_dismissed(self):
""":class:`bool`: Returns ``True`` if the user has dismissed the premium promo."""
""":class:`bool`: Returns ``True`` if the user has dismissed the current premium promotion."""
return UserFlags.premium_promo_dismissed.value
@flag_value
@ -542,6 +546,11 @@ class PrivateUserFlags(PublicUserFlags):
""":class:`bool`: Returns ``True`` if the user bought premium but has it manually disabled."""
return UserFlags.disable_premium.value
@flag_value
def quarantined(self):
""":class:`bool`: Returns ``True`` if the user is quarantined."""
return UserFlags.quarantined.value
@fill_with_flags()
class PremiumUsageFlags(BaseFlags):
@ -578,17 +587,17 @@ class PremiumUsageFlags(BaseFlags):
@flag_value
def premium_discriminator(self):
""":class:`bool`: Returns ``True`` if the user utilized premium discriminators."""
""":class:`bool`: Returns ``True`` if the user has utilized premium discriminators."""
return 1 << 0
@flag_value
def animated_avatar(self):
""":class:`bool`: Returns ``True`` if the user utilized animated avatars."""
""":class:`bool`: Returns ``True`` if the user has utilized animated avatars."""
return 1 << 1
@flag_value
def profile_banner(self):
""":class:`bool`: Returns ``True`` if the user utilized profile banners."""
""":class:`bool`: Returns ``True`` if the user has utilized profile banners."""
return 1 << 2
@ -627,19 +636,24 @@ class PurchasedFlags(BaseFlags):
@flag_value
def nitro_classic(self):
""":class:`bool`: Returns ``True`` if the user has previously purchased Nitro classic."""
""":class:`bool`: Returns ``True`` if the user has purchased Nitro classic."""
return 1 << 0
@flag_value
def nitro(self):
""":class:`bool`: Returns ``True`` if the user has previously purchased Nitro."""
""":class:`bool`: Returns ``True`` if the user has purchased Nitro."""
return 1 << 1
@flag_value
def guild_boost(self):
""":class:`bool`: Returns ``True`` if the user has previously purchased a guild boost."""
""":class:`bool`: Returns ``True`` if the user has purchased a guild boost."""
return 1 << 2
@flag_value
def nitro_basic(self):
""":class:`bool`: Returns ``True`` if the user has purchased Nitro basic."""
return 1 << 3
@fill_with_flags()
class MemberCacheFlags(BaseFlags):
@ -772,6 +786,53 @@ class ApplicationFlags(BaseFlags):
rather than using this raw value.
"""
__slots__ = ()
@flag_value
def embedded_released(self):
""":class:`bool`: Returns ``True`` if the embedded application is released to the public."""
return 1 << 1
@flag_value
def managed_emoji(self):
""":class:`bool`: Returns ``True`` if the application has the ability to create Twitch-style emotes."""
return 1 << 2
@flag_value
def embedded_iap(self):
""":class:`bool`: Returns ``True`` if the application has the ability to use embedded in-app purchases."""
return 1 << 3
@flag_value
def group_dm_create(self):
""":class:`bool`: Returns ``True`` if the application has the ability to create group DMs."""
return 1 << 4
@flag_value
def rpc_private_beta(self):
""":class:`bool`: Returns ``True`` if the application has the ability to access the client RPC server."""
return 1 << 5
@flag_value
def allow_assets(self):
""":class:`bool`: Returns ``True`` if the application has the ability to use activity assets."""
return 1 << 8
@flag_value
def allow_activity_action_spectate(self):
""":class:`bool`: Returns ``True`` if the application has the ability to enable spectating activities."""
return 1 << 9
@flag_value
def allow_activity_action_join_request(self):
""":class:`bool`: Returns ``True`` if the application has the ability to enable activity join requests."""
return 1 << 10
@flag_value
def rpc_has_connected(self):
""":class:`bool`: Returns ``True`` if the application has accessed the client RPC server before."""
return 1 << 11
@flag_value
def gateway_presence(self):
""":class:`bool`: Returns ``True`` if the application is verified and is allowed to
@ -779,11 +840,6 @@ class ApplicationFlags(BaseFlags):
"""
return 1 << 12
@alias_flag_value
def presence(self):
""":class:`bool`: Alias for :attr:`gateway_presence`."""
return 1 << 12
@flag_value
def gateway_presence_limited(self):
""":class:`bool`: Returns ``True`` if the application is allowed to receive
@ -791,11 +847,6 @@ class ApplicationFlags(BaseFlags):
"""
return 1 << 13
@alias_flag_value
def presence_limited(self):
""":class:`bool`: Alias for :attr:`gateway_presence_limited`."""
return 1 << 13
@flag_value
def gateway_guild_members(self):
""":class:`bool`: Returns ``True`` if the application is verified and is allowed to
@ -803,11 +854,6 @@ class ApplicationFlags(BaseFlags):
"""
return 1 << 14
@alias_flag_value
def guild_members(self):
""":class:`bool`: Alias for :attr:`gateway_guild_members`."""
return 1 << 14
@flag_value
def gateway_guild_members_limited(self):
""":class:`bool`: Returns ``True`` if the application is allowed to receive full
@ -815,11 +861,6 @@ class ApplicationFlags(BaseFlags):
"""
return 1 << 15
@alias_flag_value
def guild_members_limited(self):
""":class:`bool`: Alias for :attr:`gateway_guild_members_limited`."""
return 1 << 15
@flag_value
def verification_pending_guild_limit(self):
""":class:`bool`: Returns ``True`` if the application is currently pending verification
@ -838,31 +879,28 @@ class ApplicationFlags(BaseFlags):
receive message content."""
return 1 << 18
@alias_flag_value
def message_content(self):
""":class:`bool`: Alias for :attr:`gateway_message_content`."""
return 1 << 18
@flag_value
def gateway_message_content_limited(self):
""":class:`bool`: Returns ``True`` if the application is allowed to
read message content in guilds."""
return 1 << 19
@alias_flag_value
def message_content_limited(self):
""":class:`bool`: Alias for :attr:`gateway_message_content_limited`."""
return 1 << 19
@flag_value
def embedded_first_party(self):
""":class:`bool`: Returns ``True`` if the embedded application is published by Discord."""
return 1 << 20
@flag_value
def embedded_released(self):
""":class:`bool`: Returns ``True`` if the embedded application is released to the public."""
return 1 << 1
def application_command_badge(self):
""":class:`bool`: Returns ``True`` if the application has registered global application commands."""
return 1 << 23
@flag_value
def active(self):
""":class:`bool`: Returns ``True`` if the application is considered active.
This means that it has had any global command executed in the past 30 days.
"""
return 1 << 24
@fill_with_flags()
@ -895,7 +933,485 @@ class ChannelFlags(BaseFlags):
rather than using this raw value.
"""
__slots__ = ()
@flag_value
def pinned(self):
""":class:`bool`: Returns ``True`` if the thread is pinned to the forum channel."""
return 1 << 1
@fill_with_flags()
class PaymentSourceFlags(BaseFlags):
r"""Wraps up the Discord payment source flags.
.. container:: operations
.. describe:: x == y
Checks if two PaymentSourceFlags are equal.
.. describe:: x != y
Checks if two PaymentSourceFlags are not equal.
.. describe:: hash(x)
Return the flag's hash.
.. describe:: iter(x)
Returns an iterator of ``(name, value)`` pairs. This allows it
to be, for example, constructed as a dict or a list of pairs.
Note that aliases are not shown.
.. versionadded:: 2.0
Attributes
-----------
value: :class:`int`
The raw value. This value is a bit array field of a 53-bit integer
representing the currently available flags. You should query
flags via the properties rather than using this raw value.
"""
__slots__ = ()
@flag_value
def new(self):
""":class:`bool`: Returns ``True`` if the payment source is new."""
return 1 << 0
@flag_value
def unknown(self):
return 1 << 1
@fill_with_flags()
class SKUFlags(BaseFlags):
r"""Wraps up the Discord SKU flags.
.. container:: operations
.. describe:: x == y
Checks if two SKUFlags are equal.
.. describe:: x != y
Checks if two SKUFlags are not equal.
.. describe:: hash(x)
Return the flag's hash.
.. describe:: iter(x)
Returns an iterator of ``(name, value)`` pairs. This allows it
to be, for example, constructed as a dict or a list of pairs.
Note that aliases are not shown.
.. versionadded:: 2.0
Attributes
-----------
value: :class:`int`
The raw value. This value is a bit array field of a 53-bit integer
representing the currently available flags. You should query
flags via the properties rather than using this raw value.
"""
__slots__ = ()
@flag_value
def premium_purchase(self):
""":class:`bool`: Returns ``True`` if the SKU is a premium purchase."""
return 1 << 0
@flag_value
def free_premium_content(self):
""":class:`bool`: Returns ``True`` if the SKU is free premium content."""
return 1 << 1
@flag_value
def available(self):
""":class:`bool`: Returns ``True`` if the SKU is available for purchase."""
return 1 << 2
@flag_value
def premium_and_distribution(self):
""":class:`bool`: Returns ``True`` if the SKU is a premium or distribution product."""
return 1 << 3
@flag_value
def sticker_pack(self):
""":class:`bool`: Returns ``True`` if the SKU is a premium sticker pack."""
return 1 << 4
@flag_value
def guild_role_subscription(self):
""":class:`bool`: Returns ``True`` if the SKU is a guild role subscription. These are subscriptions made to guilds for premium perks."""
return 1 << 5
@flag_value
def premium_subscription(self):
""":class:`bool`: Returns ``True`` if the SKU is a Discord premium subscription or related first-party product.
These are subscriptions like Nitro and Server Boosts. These are the only giftable subscriptions.
"""
return 1 << 6
@flag_value
def application_subscription(self):
""":class:`bool`: Returns ``True`` if the SKU is a application subscription. These are subscriptions made to applications for premium perks."""
return 1 << 7
@fill_with_flags()
class PaymentFlags(BaseFlags):
r"""Wraps up the Discord payment flags.
.. container:: operations
.. describe:: x == y
Checks if two PaymentFlags are equal.
.. describe:: x != y
Checks if two PaymentFlags are not equal.
.. describe:: hash(x)
Return the flag's hash.
.. describe:: iter(x)
Returns an iterator of ``(name, value)`` pairs. This allows it
to be, for example, constructed as a dict or a list of pairs.
Note that aliases are not shown.
.. versionadded:: 2.0
Attributes
-----------
value: :class:`int`
The raw value. This value is a bit array field of a 53-bit integer
representing the currently available flags. You should query
flags via the properties rather than using this raw value.
"""
__slots__ = ()
@flag_value
def gift(self):
""":class:`bool`: Returns ``True`` if the payment is for a gift."""
return 1 << 0
@flag_value
def preorder(self):
""":class:`bool`: Returns ``True`` if the payment is a preorder."""
return 1 << 3
# TODO: The below are assumptions
@flag_value
def temporary_authorization(self):
""":class:`bool`: Returns ``True`` if the payment is a temporary authorization."""
return 1 << 5
@flag_value
def unknown(self):
return 1 << 6
@fill_with_flags()
class PromotionFlags(BaseFlags):
r"""Wraps up the Discord promotion flags.
.. container:: operations
.. describe:: x == y
Checks if two PromotionFlags are equal.
.. describe:: x != y
Checks if two PromotionFlags are not equal.
.. describe:: hash(x)
Return the flag's hash.
.. describe:: iter(x)
Returns an iterator of ``(name, value)`` pairs. This allows it
to be, for example, constructed as a dict or a list of pairs.
Note that aliases are not shown.
.. versionadded:: 2.0
Attributes
-----------
value: :class:`int`
The raw value. This value is a bit array field of a 53-bit integer
representing the currently available flags. You should query
flags via the properties rather than using this raw value.
"""
__slots__ = ()
@flag_value
def unknown_0(self):
return 1 << 0
@flag_value
def unknown_1(self):
# Possibly one month duration?
return 1 << 1
@flag_value
def unknown_2(self):
return 1 << 2
@flag_value
def unknown_3(self):
return 1 << 3
@flag_value
def unknown_4(self):
# Possibly unavailable/ended/inactive
# Maybe also direct link
# Maybe also available for existing users
return 1 << 4
@flag_value
def blocked_ios(self):
""":class:`bool`: Returns ``True`` if the promotion is blocked on iOS."""
return 1 << 5
@flag_value
def outbound_redeemable_by_trial_users(self):
""":class:`bool`: Returns ``True`` if the promotion is redeemable by trial users."""
return 1 << 6
@fill_with_flags()
class GiftFlags(BaseFlags):
r"""Wraps up the Discord payment flags.
.. container:: operations
.. describe:: x == y
Checks if two PaymentFlags are equal.
.. describe:: x != y
Checks if two PaymentFlags are not equal.
.. describe:: hash(x)
Return the flag's hash.
.. describe:: iter(x)
Returns an iterator of ``(name, value)`` pairs. This allows it
to be, for example, constructed as a dict or a list of pairs.
Note that aliases are not shown.
.. versionadded:: 2.0
Attributes
-----------
value: :class:`int`
The raw value. This value is a bit array field of a 53-bit integer
representing the currently available flags. You should query
flags via the properties rather than using this raw value.
"""
__slots__ = ()
@flag_value
def payment_source_required(self):
""":class:`bool`: Returns ``True`` if the gift requires a payment source to redeem."""
return 1 << 0
@flag_value
def existing_subscription_disallowed(self):
""":class:`bool`: Returns ``True`` if the gift cannot be redeemed by users with existing premium subscriptions."""
return 1 << 1
@flag_value
def not_self_redeemable(self):
""":class:`bool`: Returns ``True`` if the gift cannot be redeemed by the gifter."""
return 1 << 2
# TODO: The below are assumptions
@flag_value
def promotion(self):
""":class:`bool`: Returns ``True`` if the gift is from a promotion."""
return 1 << 3
@fill_with_flags()
class LibraryApplicationFlags(BaseFlags):
r"""Wraps up the Discord library application flags.
.. container:: operations
.. describe:: x == y
Checks if two LibraryApplicationFlags are equal.
.. describe:: x != y
Checks if two LibraryApplicationFlags are not equal.
.. describe:: hash(x)
Return the flag's hash.
.. describe:: iter(x)
Returns an iterator of ``(name, value)`` pairs. This allows it
to be, for example, constructed as a dict or a list of pairs.
Note that aliases are not shown.
.. versionadded:: 2.0
Attributes
-----------
value: :class:`int`
The raw value. This value is a bit array field of a 53-bit integer
representing the currently available flags. You should query
flags via the properties rather than using this raw value.
"""
__slots__ = ()
@flag_value
def hidden(self):
""":class:`bool`: Returns ``True`` if the library application is hidden."""
return 1 << 0
@flag_value
def private(self):
""":class:`bool`: Returns ``True`` if the library application is not shown in playing status."""
return 1 << 1
@flag_value
def overlay_disabled(self):
""":class:`bool`: Returns ``True`` if the library application has the Discord overlay disabled."""
return 1 << 2
@flag_value
def entitled(self):
""":class:`bool`: Returns ``True`` if the library application is entitled to the user."""
return 1 << 3
@flag_value
def premium(self):
""":class:`bool`: Returns ``True`` if the library application is free for premium users."""
return 1 << 4
@fill_with_flags()
class ApplicationDiscoveryFlags(BaseFlags):
r"""Wraps up the Discord application discovery eligibility flags.
.. container:: operations
.. describe:: x == y
Checks if two LibraryApplicationFlags are equal.
.. describe:: x != y
Checks if two LibraryApplicationFlags are not equal.
.. describe:: hash(x)
Return the flag's hash.
.. describe:: iter(x)
Returns an iterator of ``(name, value)`` pairs. This allows it
to be, for example, constructed as a dict or a list of pairs.
Note that aliases are not shown.
.. versionadded:: 2.0
Attributes
-----------
value: :class:`int`
The raw value. This value is a bit array field of a 53-bit integer
representing the currently available flags. You should query
flags via the properties rather than using this raw value.
"""
__slots__ = ()
@flag_value
def verified(self):
""":class:`bool`: Returns ``True`` if the application is verified."""
return 1 << 0
@flag_value
def tag(self):
""":class:`bool`: Returns ``True`` if the application has at least one tag set."""
return 1 << 1
@flag_value
def description(self):
""":class:`bool`: Returns ``True`` if the application has a description."""
return 1 << 2
@flag_value
def terms_of_service(self):
""":class:`bool`: Returns ``True`` if the application has a terms of service."""
return 1 << 3
@flag_value
def privacy_policy(self):
""":class:`bool`: Returns ``True`` if the application has a privacy policy."""
return 1 << 4
@flag_value
def install_params(self):
""":class:`bool`: Returns ``True`` if the application has a custom install URL or install parameters."""
return 1 << 5
@flag_value
def safe_name(self):
""":class:`bool`: Returns ``True`` if the application name is safe for work."""
return 1 << 6
@flag_value
def safe_description(self):
""":class:`bool`: Returns ``True`` if the application description is safe for work."""
return 1 << 7
@flag_value
def approved_commands(self):
""":class:`bool`: Returns ``True`` if the application has the message content intent approved or utilizes application commands."""
return 1 << 8
@flag_value
def support_guild(self):
""":class:`bool`: Returns ``True`` if the application has a support guild set."""
return 1 << 9
@flag_value
def safe_commands(self):
""":class:`bool`: Returns ``True`` if the application's commands are safe for work."""
return 1 << 10
@flag_value
def mfa(self):
""":class:`bool`: Returns ``True`` if the application's owner has MFA enabled."""
return 1 << 11
@flag_value
def safe_directory_overview(self):
""":class:`bool`: Returns ``True`` if the application's directory long description is safe for work."""
return 1 << 12
@flag_value
def supported_locales(self):
""":class:`bool`: Returns ``True`` if the application has at least one supported locale set."""
return 1 << 13
@flag_value
def safe_short_description(self):
""":class:`bool`: Returns ``True`` if the application's directory short description is safe for work."""
return 1 << 14
@flag_value
def safe_role_connections(self):
""":class:`bool`: Returns ``True`` if the application's role connections metadata is safe for work."""
return 1 << 15
@flag_value
def eligible(self):
""":class:`bool`: Returns ``True`` if the application has met all the above criteria and is eligible for discovery."""
return 1 << 16

177
discord/guild.py

@ -89,6 +89,9 @@ from .object import OLDEST_OBJECT, Object
from .profile import MemberProfile
from .partial_emoji import PartialEmoji
from .welcome_screen import *
from .appinfo import PartialApplication
from .guild_premium import PremiumGuildSubscription
from .entitlements import Entitlement
# fmt: off
@ -105,7 +108,7 @@ if TYPE_CHECKING:
from .abc import Snowflake, SnowflakeTime
from .types.guild import (
Guild as GuildPayload,
GuildPreview as GuildPreviewPayload,
PartialGuild as PartialGuildPayload,
RolePositionUpdate as RolePositionUpdatePayload,
)
from .types.threads import (
@ -119,6 +122,7 @@ if TYPE_CHECKING:
from .state import ConnectionState
from .voice_client import VoiceProtocol
from .settings import GuildSettings
from .enums import ApplicationType
from .types.channel import (
GuildChannel as GuildChannelPayload,
TextChannel as TextChannelPayload,
@ -335,7 +339,7 @@ class Guild(Hashable):
3: _GuildLimit(emoji=250, stickers=60, bitrate=384e3, filesize=104857600),
}
def __init__(self, *, data: Union[GuildPayload, GuildPreviewPayload], state: ConnectionState) -> None:
def __init__(self, *, data: Union[GuildPayload, PartialGuildPayload], state: ConnectionState) -> None:
self._chunked = False
self._cs_joined: Optional[bool] = None
self._roles: Dict[int, Role] = {}
@ -446,7 +450,7 @@ class Guild(Hashable):
return role
def _from_data(self, guild: Union[GuildPayload, GuildPreviewPayload]) -> None:
def _from_data(self, guild: Union[GuildPayload, PartialGuildPayload]) -> None:
try:
self._member_count: int = guild['member_count'] # type: ignore # Handled below
except KeyError:
@ -874,7 +878,7 @@ class Guild(Hashable):
@property
def premium_subscribers(self) -> List[Member]:
"""List[:class:`Member`]: A list of members who have "boosted" this guild."""
"""List[:class:`Member`]: A list of members who have subscribed to (boosted) this guild."""
return [member for member in self.members if member.premium_since is not None]
@property
@ -2346,10 +2350,10 @@ class Guild(Hashable):
return Template(state=self._state, data=data)
async def create_integration(self, *, type: IntegrationType, id: int) -> None:
async def create_integration(self, *, type: IntegrationType, id: int, reason: Optional[str] = None) -> None:
"""|coro|
Attaches an integration to the guild.
Attaches an integration to the guild. This "enables" an existing integration.
You must have the :attr:`~Permissions.manage_guild` permission to
do this.
@ -2362,6 +2366,10 @@ class Guild(Hashable):
The integration type (e.g. Twitch).
id: :class:`int`
The integration ID.
reason: Optional[:class:`str`]
The reason for creating this integration. Shows up on the audit log.
.. versionadded:: 2.0
Raises
-------
@ -2370,7 +2378,7 @@ class Guild(Hashable):
HTTPException
The account could not be found.
"""
await self._state.http.create_integration(self.id, type, id)
await self._state.http.create_integration(self.id, type, id, reason=reason)
async def integrations(self, *, with_applications=True) -> List[Integration]:
"""|coro|
@ -3557,6 +3565,161 @@ class Guild(Hashable):
if payload:
await self._state.http.edit_welcome_screen(self.id, payload)
async def applications(
self, *, with_team: bool = False, type: Optional[ApplicationType] = None, channel: Optional[Snowflake] = None
) -> List[PartialApplication]:
"""|coro|
Returns the list of applications that are attached to this guild.
.. versionadded:: 2.0
Parameters
-----------
with_team: :class:`bool`
Whether to include the team of the application.
type: :class:`ApplicationType`
The type of application to restrict the returned applications to.
Raises
-------
HTTPException
Fetching the applications failed.
Returns
--------
List[:class:`PartialApplication`]
The applications that belong to this guild.
"""
data = await self._state.http.get_guild_applications(
self.id, include_team=with_team, type=int(type) if type else None, channel_id=channel.id if channel else None
)
return [PartialApplication(state=self._state, data=app) for app in data]
async def premium_subscriptions(self) -> List[PremiumGuildSubscription]:
"""|coro|
Returns the list of premium subscriptions (boosts) for this guild.
.. versionadded:: 2.0
Raises
-------
Forbidden
You do not have permission to get the premium guild subscriptions.
HTTPException
Fetching the premium guild subscriptions failed.
Returns
--------
List[:class:`PremiumGuildSubscription`]
The premium guild subscriptions.
"""
data = await self._state.http.get_guild_subscriptions(self.id)
return [PremiumGuildSubscription(state=self._state, data=sub) for sub in data]
async def apply_premium_subscription_slots(self, *subscription_slots: Snowflake) -> List[PremiumGuildSubscription]:
r"""|coro|
Applies premium subscription slots to the guild (boosts the guild).
.. versionadded:: 2.0
Parameters
-----------
\*subscription_slots: :class:`PremiumGuildSubscriptionSlot`
The subscription slots to apply.
Raises
-------
HTTPException
Applying the premium subscription slots failed.
"""
if not subscription_slots:
return []
data = await self._state.http.apply_guild_subscription_slots(self.id, [slot.id for slot in subscription_slots])
return [PremiumGuildSubscription(state=self._state, data=sub) for sub in data]
async def entitlements(
self, *, with_sku: bool = True, with_application: bool = True, exclude_deleted: bool = False
) -> List[Entitlement]:
"""|coro|
Returns the list of entitlements for this guild.
.. versionadded:: 2.0
Parameters
-----------
with_sku: :class:`bool`
Whether to include the SKU information in the returned entitlements.
with_application: :class:`bool`
Whether to include the application in the returned entitlements' SKUs.
exclude_deleted: :class:`bool`
Whether to exclude deleted entitlements.
Raises
-------
HTTPException
Retrieving the entitlements failed.
Returns
-------
List[:class:`Entitlement`]
The guild's entitlements.
"""
state = self._state
data = await state.http.get_guild_entitlements(
self.id, with_sku=with_sku, with_application=with_application, exclude_deleted=exclude_deleted
)
return [Entitlement(state=state, data=d) for d in data]
async def price_tiers(self) -> List[int]:
"""|coro|
Returns the list of price tiers available for use in this guild.
.. versionadded:: 2.0
Raises
-------
HTTPException
Fetching the price tiers failed.
Returns
--------
List[:class:`int`]
The available price tiers.
"""
return await self._state.http.get_price_tiers(1, self.id)
async def fetch_price_tier(self, price_tier: int, /) -> Dict[str, int]:
"""|coro|
Returns a mapping of currency to price for the given price tier.
.. versionadded:: 2.0
Parameters
-----------
price_tier: :class:`int`
The price tier to retrieve.
Raises
-------
NotFound
The price tier does not exist.
HTTPException
Fetching the price tier failed.
Returns
-------
Dict[:class:`str`, :class:`int`]
The price tier mapping.
"""
return await self._state.http.get_price_tier(price_tier)
async def chunk(self, channel: Snowflake = MISSING) -> List[Member]:
"""|coro|

320
discord/guild_premium.py

@ -0,0 +1,320 @@
"""
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, Optional
from .mixins import Hashable
from .subscriptions import Subscription
from .utils import parse_time, utcnow
if TYPE_CHECKING:
from .abc import Snowflake
from .guild import Guild
from .state import ConnectionState
from .types.subscriptions import (
PremiumGuildSubscription as PremiumGuildSubscriptionPayload,
PremiumGuildSubscriptionSlot as PremiumGuildSubscriptionSlotPayload,
PremiumGuildSubscriptionCooldown as PremiumGuildSubscriptionCooldownPayload,
)
__all__ = (
'PremiumGuildSubscription',
'PremiumGuildSubscriptionSlot',
'PremiumGuildSubscriptionCooldown',
)
class PremiumGuildSubscription(Hashable):
"""Represents a premium guild subscription (boost).
.. container:: operations
.. describe:: x == y
Checks if two premium guild subscriptions are equal.
.. describe:: x != y
Checks if two premium guild subscriptions are not equal.
.. describe:: hash(x)
Returns the premium guild subscription's hash.
.. versionadded:: 2.0
Attributes
------------
id: :class:`int`
The ID of the guild premium subscription.
guild_id: :class:`int`
The ID of the guild this guild premium subscription belongs to.
user_id: :class:`int`
The ID of the user this guild premium subscription belongs to.
user: :class:`User`
The user this guild premium subscription belongs to.
ended: :class:`bool`
Whether the guild premium subscription has ended.
ends_at: Optional[:class:`datetime.datetime`]
When the guild premium subscription ends.
"""
def __init__(self, *, state: ConnectionState, data: PremiumGuildSubscriptionPayload):
self._state = state
self._update(data)
def _update(self, data: PremiumGuildSubscriptionPayload):
state = self._state
self.id = int(data['id'])
self.guild_id = int(data['guild_id'])
self.user_id = int(data['user_id'])
self.user = state.store_user(data['user']) if 'user' in data else state.user
self.ended = data.get('ended', False)
self.ends_at: Optional[datetime] = parse_time(data.get('ends_at'))
def __repr__(self) -> str:
return f'<PremiumGuildSubscription id={self.id} guild_id={self.guild_id} user_id={self.user_id} ended={self.ended}>'
@property
def guild(self) -> Optional[Guild]:
"""Optional[:class:`Guild`]: The guild this guild premium subscription belongs to, if available."""
return self._state._get_guild(self.guild_id)
@property
def remaining(self) -> Optional[timedelta]:
"""Optional[:class:`datetime.timedelta`]: The remaining time for this guild premium subscription.
This is ``None`` if the subscription is not ending.
"""
if self.ends_at is None or self.ends_at <= utcnow():
return None
return self.ends_at - utcnow()
async def delete(self) -> None:
"""|coro|
Deletes this guild premium subscription.
Raises
-------
Forbidden
You do not have permissions to delete this guild premium subscription.
HTTPException
Deleting the guild premium subscription failed.
"""
await self._state.http.delete_guild_subscription(self.guild_id, self.id)
class PremiumGuildSubscriptionSlot(Hashable):
"""Represents a premium guild subscription (boost) slot.
This is a slot that can be used on a guild (to boost it).
.. container:: operations
.. describe:: x == y
Checks if two subscription slots are equal.
.. describe:: x != y
Checks if two subscription slots are not equal.
.. describe:: hash(x)
Returns the subscription slot's hash.
.. versionadded:: 2.0
Attributes
------------
id: :class:`int`
The ID of the guild subscription slot.
subscription_id: :class:`int`
The ID of the guild subscription this slot belongs to.
canceled: :class:`bool`
Whether the slot is canceled.
cooldown_ends_at: Optional[:class:`datetime.datetime`]
When the cooldown for this guild subscription slot ends.
premium_guild_subscription: Optional[:class:`PremiumGuildSubscription`]
The subscription this slot belongs to.
"""
__slots__ = (
'id',
'subscription_id',
'canceled',
'cooldown_ends_at',
'premium_guild_subscription',
'_state',
)
def __init__(self, *, state: ConnectionState, data: PremiumGuildSubscriptionSlotPayload):
self._state = state
self._update(data)
def _update(self, data: PremiumGuildSubscriptionSlotPayload):
self.id = int(data['id'])
self.subscription_id = int(data['subscription_id'])
self.canceled = data.get('canceled', False)
self.cooldown_ends_at: Optional[datetime] = parse_time(data.get('cooldown_ends_at'))
premium_guild_subscription = data.get('premium_guild_subscription')
self.premium_guild_subscription: Optional[PremiumGuildSubscription] = (
PremiumGuildSubscription(state=self._state, data=premium_guild_subscription)
if premium_guild_subscription is not None
else None
)
def __repr__(self) -> str:
return f'<PremiumGuildSubscriptionSlot id={self.id} subscription_id={self.subscription_id} canceled={self.canceled}>'
def is_available(self) -> bool:
""":class:`bool`: Indicates if the slot is available for use."""
return not self.premium_guild_subscription and not self.is_on_cooldown()
def is_on_cooldown(self) -> bool:
""":class:`bool`: Indicates if the slot is on cooldown."""
return self.cooldown_ends_at is not None and self.cooldown_ends_at > utcnow()
@property
def cancelled(self) -> bool:
""":class:`bool`: Whether the slot is cancelled.
This is an alias of :attr:`canceled`.
"""
return self.canceled
@property
def cooldown_remaining(self) -> Optional[timedelta]:
"""Optional[:class:`datetime.timedelta`]: The cooldown remaining for this boost slot.
This is ``None`` if the cooldown has ended.
"""
if self.cooldown_ends_at is None or self.cooldown_ends_at <= utcnow():
return None
return self.cooldown_ends_at - utcnow()
async def subscription(self) -> Subscription:
"""|coro|
Retrieves the subscription this guild subscription slot is attached to.
Raises
------
NotFound
You cannot access this subscription.
HTTPException
Fetching the subscription failed.
Returns
-------
:class:`Subscription`
The retrieved subscription, if applicable.
"""
data = await self._state.http.get_subscription(self.subscription_id)
return Subscription(data=data, state=self._state)
async def apply(self, guild: Snowflake) -> PremiumGuildSubscription:
"""|coro|
Applies the premium guild subscription slot to a guild.
Parameters
-----------
guild: :class:`Guild`
The guild to apply the slot to.
Raises
-------
HTTPException
Applying the slot failed.
Returns
--------
:class:`PremiumGuildSubscription`
The premium guild subscription that was created.
"""
state = self._state
data = await state.http.apply_guild_subscription_slots(guild.id, (self.id,))
return PremiumGuildSubscription(state=state, data=data[0])
async def cancel(self) -> None:
"""|coro|
Cancels the guild subscription slot.
Raises
-------
HTTPException
Cancelling the slot failed.
"""
data = await self._state.http.cancel_guild_subscription_slot(self.id)
self._update(data)
async def uncancel(self) -> None:
"""|coro|
Uncancels the guild subscription slot.
Raises
-------
HTTPException
Uncancelling the slot failed.
"""
data = await self._state.http.uncancel_guild_subscription_slot(self.id)
self._update(data)
class PremiumGuildSubscriptionCooldown:
"""Represents a premium guild subscription cooldown.
This is a cooldown that is applied to your guild subscription slot changes (boosting and unboosting).
.. versionadded:: 2.0
Attributes
------------
ends_at: :class:`datetime.datetime`
When the cooldown resets.
limit: :class:`int`
The maximum number of changes that can be made before the cooldown is applied.
remaining: :class:`int`
The number of changes remaining before the cooldown is applied.
"""
def __init__(self, *, state: ConnectionState, data: PremiumGuildSubscriptionCooldownPayload):
self._state = state
self._update(data)
def _update(self, data: PremiumGuildSubscriptionCooldownPayload):
self.ends_at: datetime = parse_time(data['ends_at'])
self.limit = data['limit']
self.remaining = data.get('remaining', 0)

1591
discord/http.py

File diff suppressed because it is too large

160
discord/integrations.py

@ -24,31 +24,32 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
import datetime
from typing import Any, Dict, Optional, TYPE_CHECKING, Type, Tuple
from .utils import _get_as_snowflake, parse_time, MISSING
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type
from .enums import ExpireBehaviour, try_enum
from .user import User
from .enums import try_enum, ExpireBehaviour
from .utils import MISSING, _get_as_snowflake, parse_time, utcnow
__all__ = (
'IntegrationAccount',
'IntegrationApplication',
'Integration',
'StreamIntegration',
'BotIntegration',
)
if TYPE_CHECKING:
from datetime import datetime
from .appinfo import IntegrationApplication
from .guild import Guild
from .role import Role
from .state import ConnectionState
from .types.integration import (
IntegrationAccount as IntegrationAccountPayload,
Integration as IntegrationPayload,
StreamIntegration as StreamIntegrationPayload,
BotIntegration as BotIntegrationPayload,
Integration as IntegrationPayload,
IntegrationAccount as IntegrationAccountPayload,
IntegrationType,
IntegrationApplication as IntegrationApplicationPayload,
StreamIntegration as StreamIntegrationPayload,
)
@ -89,13 +90,13 @@ class Integration:
guild: :class:`Guild`
The guild of the integration.
type: :class:`str`
The integration type (i.e. Twitch).
The integration type.
enabled: :class:`bool`
Whether the integration is currently enabled.
account: :class:`IntegrationAccount`
The account linked to this integration.
user: :class:`User`
The user that added this integration.
user: Optional[:class:`User`]
The user that added this integration, if available.
"""
__slots__ = (
@ -125,7 +126,7 @@ class Integration:
user = data.get('user')
self.user: Optional[User] = User(state=self._state, data=user) if user else None
self.enabled: bool = data['enabled']
self.enabled: bool = data.get('enabled', True)
async def delete(self, *, reason: Optional[str] = None) -> None:
"""|coro|
@ -150,6 +151,7 @@ class Integration:
Deleting the integration failed.
"""
await self._state.http.delete_integration(self.guild.id, self.id, reason=reason)
self.enabled = False
class StreamIntegration(Integration):
@ -171,14 +173,14 @@ class StreamIntegration(Integration):
Whether the integration is currently enabled.
syncing: :class:`bool`
Where the integration is currently syncing.
enable_emoticons: Optional[:class:`bool`]
enable_emoticons: :class:`bool`
Whether emoticons should be synced for this integration (currently twitch only).
expire_behaviour: :class:`ExpireBehaviour`
The behaviour of expiring subscribers. Aliased to ``expire_behavior`` as well.
expire_grace_period: :class:`int`
The grace period (in days) for expiring subscribers.
user: :class:`User`
The user for the integration.
user: Optional[:class:`User`]
The user for the integration, if available.
account: :class:`IntegrationAccount`
The integration account information.
synced_at: :class:`datetime.datetime`
@ -198,14 +200,14 @@ class StreamIntegration(Integration):
def _from_data(self, data: StreamIntegrationPayload) -> None:
super()._from_data(data)
self.revoked: bool = data['revoked']
self.expire_behaviour: ExpireBehaviour = try_enum(ExpireBehaviour, data['expire_behavior'])
self.expire_grace_period: int = data['expire_grace_period']
self.synced_at: datetime.datetime = parse_time(data['synced_at'])
self.revoked: bool = data.get('revoked', False)
self.expire_behaviour: ExpireBehaviour = try_enum(ExpireBehaviour, data.get('expire_behaviour', 0))
self.expire_grace_period: int = data.get('expire_grace_period', 1)
self.synced_at: datetime = parse_time(data['synced_at']) if 'synced_at' in data else utcnow()
self._role_id: Optional[int] = _get_as_snowflake(data, 'role_id')
self.syncing: bool = data['syncing']
self.enable_emoticons: bool = data['enable_emoticons']
self.subscriber_count: int = data['subscriber_count']
self.syncing: bool = data.get('syncing', False)
self.enable_emoticons: bool = data.get('enable_emoticons', True)
self.subscriber_count: int = data.get('subscriber_count', 0)
@property
def expire_behavior(self) -> ExpireBehaviour:
@ -232,10 +234,6 @@ class StreamIntegration(Integration):
You must have the :attr:`~Permissions.manage_guild` permission to
do this.
.. versionchanged:: 2.0
This function will now raise :exc:`TypeError` instead of
``InvalidArgument``.
Parameters
-----------
expire_behaviour: :class:`ExpireBehaviour`
@ -251,15 +249,10 @@ class StreamIntegration(Integration):
You do not have permission to edit the integration.
HTTPException
Editing the guild failed.
TypeError
``expire_behaviour`` did not receive a :class:`ExpireBehaviour`.
"""
payload: Dict[str, Any] = {}
if expire_behaviour is not MISSING:
if not isinstance(expire_behaviour, ExpireBehaviour):
raise TypeError('expire_behaviour field must be of type ExpireBehaviour')
payload['expire_behavior'] = expire_behaviour.value
payload['expire_behavior'] = int(expire_behaviour)
if expire_grace_period is not MISSING:
payload['expire_grace_period'] = expire_grace_period
@ -267,8 +260,6 @@ class StreamIntegration(Integration):
if enable_emoticons is not MISSING:
payload['enable_emoticons'] = enable_emoticons
# This endpoint is undocumented.
# Unsure if it returns the data or not as a result
await self._state.http.edit_integration(self.guild.id, self.id, **payload)
async def sync(self) -> None:
@ -287,47 +278,54 @@ class StreamIntegration(Integration):
Syncing the integration failed.
"""
await self._state.http.sync_integration(self.guild.id, self.id)
self.synced_at = datetime.datetime.now(datetime.timezone.utc)
self.synced_at = utcnow()
async def disable(self, *, reason: Optional[str] = None) -> None:
"""|coro|
class IntegrationApplication:
"""Represents an application for a bot integration.
Disables the integration.
.. versionadded:: 2.0
This is an alias of :meth:`Integration.delete`.
Attributes
----------
id: :class:`int`
The ID for this application.
name: :class:`str`
The application's name.
icon: Optional[:class:`str`]
The application's icon hash.
description: :class:`str`
The application's description. Can be an empty string.
summary: :class:`str`
The summary of the application. Can be an empty string.
user: Optional[:class:`User`]
The bot user on this application.
"""
You must have the :attr:`~Permissions.manage_guild` permission to
do this.
__slots__ = (
'id',
'name',
'icon',
'description',
'summary',
'user',
)
Parameters
-----------
reason: :class:`str`
The reason the integration was disabled. Shows up on the audit log.
def __init__(self, *, data: IntegrationApplicationPayload, state: ConnectionState) -> None:
self.id: int = int(data['id'])
self.name: str = data['name']
self.icon: Optional[str] = data['icon']
self.description: str = data['description']
self.summary: str = data['summary']
user = data.get('bot')
self.user: Optional[User] = User(state=state, data=user) if user else None
Raises
-------
Forbidden
You do not have permission to disable the integration.
HTTPException
Disabling the integration failed.
"""
await self.delete(reason=reason)
async def enable(self, *, reason: Optional[str] = None) -> None:
"""|coro|
Enables the integration.
You must have the :attr:`~Permissions.manage_guild` permission to
do this.
Parameters
-----------
reason: :class:`str`
The reason the integration was enabled. Shows up on the audit log.
Raises
-------
Forbidden
You do not have permission to enable the integration.
HTTPException
Enabling the integration failed.
"""
await self._state.http.create_integration(self.guild.id, self.type, self.id, reason=reason)
self.enabled = True
class BotIntegration(Integration):
@ -344,22 +342,30 @@ class BotIntegration(Integration):
guild: :class:`Guild`
The guild of the integration.
type: :class:`str`
The integration type (i.e. Twitch).
The integration type (i.e. Discord).
enabled: :class:`bool`
Whether the integration is currently enabled.
user: :class:`User`
The user that added this integration.
user: Optional[:class:`User`]
The user that added this integration, if available.
account: :class:`IntegrationAccount`
The integration account information.
application: :class:`IntegrationApplication`
The application tied to this integration.
application_id: :class:`int`
The application ID of the integration.
application: Optional[:class:`IntegrationApplication`]
The application tied to this integration. Not available in some contexts.
scopes: List[:class:`str`]
The scopes the integration is authorized for.
"""
__slots__ = ('application',)
__slots__ = ('application', 'application_id', 'scopes')
def _from_data(self, data: BotIntegrationPayload) -> None:
super()._from_data(data)
self.application: IntegrationApplication = IntegrationApplication(data=data['application'], state=self._state)
self.application: Optional[IntegrationApplication] = (
self._state.create_integration_application(data['application']) if 'application' in data else None
)
self.application_id = self.application.id if self.application else int(data['application_id']) # type: ignore # One or the other
self.scopes: List[str] = data.get('scopes', [])
def _integration_factory(value: str) -> Tuple[Type[Integration], str]:

22
discord/invite.py

@ -53,6 +53,7 @@ if TYPE_CHECKING:
from .state import ConnectionState
from .guild import Guild
from .abc import GuildChannel, PrivateChannel, Snowflake
from .channel import GroupChannel
from .user import User
from .appinfo import PartialApplication
from .message import Message
@ -95,9 +96,13 @@ class PartialInviteChannel:
The partial channel's ID.
type: :class:`ChannelType`
The partial channel's type.
recipients: Optional[List[:class:`str`]]
The partial channel's recipient names. This is only applicable to group DMs.
.. versionadded:: 2.0
"""
__slots__ = ('_state', 'id', 'name', 'type', '_icon')
__slots__ = ('_state', 'id', 'name', 'type', 'recipients', '_icon')
def __new__(cls, data: Optional[InviteChannelPayload], *args, **kwargs):
if data is None:
@ -111,6 +116,9 @@ class PartialInviteChannel:
self.id: int = int(data['id'])
self.name: str = data['name']
self.type: ChannelType = try_enum(ChannelType, data['type'])
self.recipients: Optional[List[str]] = (
[user['username'] for user in data.get('recipients', [])] if self.type == ChannelType.group else None
)
self._icon: Optional[str] = data.get('icon')
def __str__(self) -> str:
@ -310,8 +318,6 @@ class Invite(Hashable):
+------------------------------------+------------------------------------------------------------+
| :attr:`approximate_presence_count` | :meth:`Client.fetch_invite` with `with_counts` enabled |
+------------------------------------+------------------------------------------------------------+
| :attr:`expires_at` | :meth:`Client.fetch_invite` with `with_expiration` enabled |
+------------------------------------+------------------------------------------------------------+
If it's not in the table above then it is available by all methods.
@ -349,12 +355,12 @@ class Invite(Hashable):
The approximate number of members currently active in the guild.
This includes idle, dnd, online, and invisible members. Offline members are excluded.
expires_at: Optional[:class:`datetime.datetime`]
The expiration date of the invite. If the value is ``None`` when received through
`Client.fetch_invite` with `with_expiration` enabled, the invite will never expire.
The expiration date of the invite. If the value is ``None`` (unless received through
`Client.fetch_invite` with `with_expiration` disabled), the invite will never expire.
.. versionadded:: 2.0
channel: Optional[Union[:class:`abc.GuildChannel`, :class:`Object`, :class:`PartialInviteChannel`]]
channel: Optional[Union[:class:`abc.GuildChannel`, :class:`GroupChannel`, :class:`Object`, :class:`PartialInviteChannel`]]
The channel the invite is for. Can be ``None`` if not a guild invite.
target_type: :class:`InviteTarget`
The type of target for the voice channel invite.
@ -435,7 +441,7 @@ class Invite(Hashable):
state: ConnectionState,
data: InvitePayload,
guild: Optional[Union[PartialInviteGuild, Guild]] = None,
channel: Optional[Union[PartialInviteChannel, GuildChannel]] = None,
channel: Optional[Union[PartialInviteChannel, GuildChannel, GroupChannel]] = None,
welcome_screen: Optional[WelcomeScreen] = None,
):
self._state: ConnectionState = state
@ -552,7 +558,7 @@ class Invite(Hashable):
def _resolve_channel(
self,
data: Optional[InviteChannelPayload],
channel: Optional[Union[PartialInviteChannel, GuildChannel]] = None,
channel: Optional[Union[PartialInviteChannel, GuildChannel, GroupChannel]] = None,
) -> Optional[InviteChannelType]:
if channel is not None:
return channel

280
discord/library.py

@ -0,0 +1,280 @@
"""
The MIT License (MIT)
Copyright (c) 2021-present Dolfies
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, List, Optional
from .appinfo import ApplicationActivityStatistics, ApplicationBranch, PartialApplication
from .entitlements import Entitlement
from .enums import SKUType, try_enum
from .flags import LibraryApplicationFlags
from .mixins import Hashable
from .utils import MISSING, _get_as_snowflake, find, parse_date, parse_time
if TYPE_CHECKING:
from datetime import date, datetime
from .asset import Asset
from .state import ConnectionState
from .types.appinfo import Branch as BranchPayload
from .types.library import LibraryApplication as LibraryApplicationPayload
from .types.store import PartialSKU as PartialSKUPayload
__all__ = (
'LibrarySKU',
'LibraryApplication',
)
class LibrarySKU(Hashable):
"""Represents a partial store SKU for a library entry.
.. container:: operations
.. describe:: x == y
Checks if two library SKUs are equal.
.. describe:: x != y
Checks if two library SKUs are not equal.
.. describe:: hash(x)
Returns the library SKU's hash.
.. versionadded:: 2.0
Attributes
-----------
id: :class:`int`
The SKU's ID.
type: :class:`SKUType`
The type of the SKU.
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.
premium: :class:`bool`
Whether this SKU is provided for free to premium users.
"""
__slots__ = (
'id',
'type',
'preorder_release_date',
'preorder_released_at',
'premium',
)
def __init__(self, data: PartialSKUPayload):
self.id: int = int(data['id'])
self.type: SKUType = try_enum(SKUType, data['type'])
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.premium: bool = data.get('premium', False)
def __repr__(self) -> str:
return f'<LibrarySKU id={self.id} type={self.type!r} preorder_release_date={self.preorder_release_date!r} preorder_released_at={self.preorder_released_at!r} premium={self.premium!r}>'
class LibraryApplication:
"""Represents a library entry.
.. container:: operations
.. describe:: x == y
Checks if two library entries are equal.
.. describe:: x != y
Checks if two library entries are not equal.
.. describe:: hash(x)
Returns the library entry's hash.
.. describe:: str(x)
Returns the library entry's name.
.. versionadded:: 2.0
Attributes
-----------
created_at: :class:`datetime.datetime`
When this library entry was created.
application: :class:`PartialApplication`
The application that this library entry is for.
sku_id: :class:`int`
The ID of the SKU that this library entry is for.
sku: :class:`LibrarySKU`
The SKU that this library entry is for.
entitlements: List[:class:`Entitlement`]
The entitlements that this library entry has.
branch_id: :class:`int`
The ID of the branch that this library entry installs.
branch: :class:`ApplicationBranch`
The branch that this library entry installs.
"""
__slots__ = (
'created_at',
'application',
'sku_id',
'sku',
'entitlements',
'branch_id',
'branch',
'_flags',
'_state',
)
def __init__(self, *, state: ConnectionState, data: LibraryApplicationPayload):
self._state = state
self._update(data)
def _update(self, data: LibraryApplicationPayload):
state = self._state
self.created_at: datetime = parse_time(data['created_at'])
self.application: PartialApplication = PartialApplication(state=state, data=data['application'])
self.sku_id: int = int(data['sku_id'])
self.sku: LibrarySKU = LibrarySKU(data=data['sku'])
self.entitlements: List[Entitlement] = [Entitlement(state=state, data=e) for e in data.get('entitlements', [])]
self._flags = data.get('flags', 0)
self.branch_id: int = int(data['branch_id'])
branch: Optional[BranchPayload] = data.get('branch')
if not branch:
branch = {'id': self.branch_id, 'name': 'master'}
self.branch: ApplicationBranch = ApplicationBranch(state=state, data=branch, application_id=self.application.id)
def __repr__(self) -> str:
return f'<LibraryApplication created_at={self.created_at!r} application={self.application!r} sku={self.sku!r} branch={self.branch!r}>'
def __eq__(self, other: Any) -> bool:
if isinstance(other, LibraryApplication):
return self.application.id == other.application.id and self.branch_id == other.branch_id
return False
def __ne__(self, other: Any) -> bool:
if isinstance(other, LibraryApplication):
return self.application.id != other.application.id or self.branch_id != other.branch_id
return True
def __hash__(self) -> int:
return hash((self.application.id, self.branch_id))
def __str__(self) -> str:
return self.application.name
@property
def name(self) -> str:
""":class:`str`: The library entry's name."""
return self.application.name
@property
def icon(self) -> Optional[Asset]:
""":class:`Asset`: The library entry's icon asset, if any."""
return self.application.icon
@property
def flags(self) -> LibraryApplicationFlags:
""":class:`LibraryApplicationFlags`: The library entry's flags."""
return LibraryApplicationFlags._from_value(self._flags)
async def activity_statistics(self) -> ApplicationActivityStatistics:
"""|coro|
Gets the activity statistics for this library entry.
Raises
-------
HTTPException
Getting the activity statistics failed.
Returns
--------
:class:`ApplicationActivityStatistics`
The activity statistics for this library entry.
"""
state = self._state
data = await state.http.get_activity_statistics()
app = find(lambda a: _get_as_snowflake(a, 'application_id') == self.application.id, data)
return ApplicationActivityStatistics(
data=app
or {'application_id': self.application.id, 'total_duration': 0, 'last_played_at': '1970-01-01T00:00:00+00:00'},
state=state,
)
async def mark_installed(self) -> None:
"""|coro|
Marks the library entry as installed.
Raises
-------
HTTPException
Marking the library entry as installed failed.
"""
await self._state.http.mark_library_entry_installed(self.application.id, self.branch_id)
async def edit(self, *, flags: LibraryApplicationFlags = MISSING) -> None:
"""|coro|
Edits the library entry.
All parameters are optional.
Parameters
-----------
flags: :class:`LibraryApplicationFlags`
The new flags to set for the library entry.
Raises
-------
HTTPException
Editing the library entry failed.
"""
payload = {}
if flags is not MISSING:
payload['flags'] = flags.value
data = await self._state.http.edit_library_entry(self.application.id, self.branch_id, payload)
self._update(data)
async def delete(self) -> None:
"""|coro|
Deletes the library entry.
Raises
-------
HTTPException
Deleting the library entry failed.
"""
await self._state.http.delete_library_entry(self.application.id, self.branch_id)

8
discord/member.py

@ -65,7 +65,7 @@ if TYPE_CHECKING:
UserWithMember as UserWithMemberPayload,
)
from .types.gateway import GuildMemberUpdateEvent
from .types.user import User as UserPayload
from .types.user import PartialUser as PartialUserPayload
from .abc import Snowflake
from .state import ConnectionState
from .message import Message
@ -106,7 +106,7 @@ class VoiceState:
suppress: :class:`bool`
Indicates if the user is suppressed from speaking.
Only applies to stage channels.
Only applicable to stage channels.
.. versionadded:: 1.7
@ -440,7 +440,7 @@ class Member(discord.abc.Messageable, discord.abc.Connectable, _UserTag):
if any(getattr(self, attr) != getattr(old, attr) for attr in attrs):
return old
def _presence_update(self, data: PartialPresenceUpdate, user: UserPayload) -> Optional[Tuple[User, User]]:
def _presence_update(self, data: PartialPresenceUpdate, user: PartialUserPayload) -> Optional[Tuple[User, User]]:
if self._self:
return
@ -450,7 +450,7 @@ class Member(discord.abc.Messageable, discord.abc.Connectable, _UserTag):
if len(user) > 1:
return self._update_inner_user(user)
def _update_inner_user(self, user: UserPayload) -> Optional[Tuple[User, User]]:
def _update_inner_user(self, user: PartialUserPayload) -> Optional[Tuple[User, User]]:
u = self._user
original = (u.name, u._avatar, u.discriminator, u._public_flags)
# These keys seem to always be available

134
discord/metadata.py

@ -0,0 +1,134 @@
"""
The MIT License (MIT)
Copyright (c) 2021-present Dolfies
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Dict, Iterator, Optional, Tuple, Union
from .utils import parse_time
class Metadata:
"""Represents a raw model from Discord.
Because of how unstable and wildly varying some metadata in Discord can be, this is a simple class
that just provides access to the raw data using dot notation. This means that ``None`` is returned
for unknown attributes instead of raising an exception. This class can be used similarly to a dictionary.
.. versionadded:: 2.0
.. container:: operations
.. describe:: x == y
Checks if two metadata objects are equal.
.. describe:: x != y
Checks if two metadata objects are not equal.
.. describe:: x[key]
Returns a metadata value if it is found, otherwise raises a :exc:`KeyError`.
.. describe:: key in x
Checks if a metadata value is present.
.. describe:: len(x)
Returns the number of metadata values present.
.. describe:: iter(x)
Returns an iterator of ``(field, value)`` pairs. This allows this class
to be used as an iterable in list/dict/etc constructions.
"""
def __init__(self, data: Optional[MetadataObject] = None) -> None:
if not data:
return
for key, value in data.items():
if isinstance(value, dict):
value = Metadata(value)
elif key.endswith('_id') and isinstance(value, str) and value.isdigit():
value = int(value)
elif (key.endswith('_at') or key.endswith('_date')) and isinstance(value, str):
try:
value = parse_time(value)
except ValueError:
pass
elif isinstance(value, list):
value = [Metadata(x) if isinstance(x, dict) else x for x in value]
self.__dict__[key] = value
def __repr__(self) -> str:
if not self.__dict__:
return '<Metadata>'
return f'<Metadata {" ".join(f"{k}={v!r}" for k, v in self.__dict__.items())}>'
def __eq__(self, other: object) -> bool:
if not isinstance(other, Metadata):
return False
return self.__dict__ == other.__dict__
def __ne__(self, other: object) -> bool:
if not isinstance(other, Metadata):
return True
return self.__dict__ != other.__dict__
def __iter__(self) -> Iterator[Tuple[str, Any]]:
yield from self.__dict__.items()
def __getitem__(self, key: str) -> Any:
return self.__dict__[key]
def __setitem__(self, key: str, value: Any) -> None:
self.__dict__[key] = value
def __getattr__(self, _) -> Any:
return None
def __contains__(self, key: str) -> bool:
return key in self.__dict__
def __len__(self) -> int:
return len(self.__dict__)
def keys(self):
"""A set-like object providing a view on the metadata's keys."""
return self.__dict__.keys()
def values(self):
"""A set-like object providing a view on the metadata's values."""
return self.__dict__.values()
def items(self):
"""A set-like object providing a view on the metadata's items."""
return self.__dict__.items()
if TYPE_CHECKING:
MetadataObject = Union[Metadata, Dict[str, Any]]

6
discord/modal.py

@ -32,7 +32,7 @@ from .mixins import Hashable
from .utils import _generate_nonce
if TYPE_CHECKING:
from .appinfo import InteractionApplication
from .appinfo import IntegrationApplication
from .components import ActionRow
from .interactions import Interaction
@ -78,7 +78,7 @@ class Modal(Hashable):
The ID of the modal that gets received during an interaction.
components: List[:class:`Component`]
A list of components in the modal.
application: :class:`InteractionApplication`
application: :class:`IntegrationApplication`
The application that sent the modal.
"""
@ -92,7 +92,7 @@ class Modal(Hashable):
self.title: str = data.get('title', '')
self.custom_id: str = data.get('custom_id', '')
self.components: List[ActionRow] = [_component_factory(d) for d in data.get('components', [])] # type: ignore # Will always be rows here
self.application: InteractionApplication = interaction._state.create_interaction_application(data['application'])
self.application: IntegrationApplication = interaction._state.create_integration_application(data['application'])
def __str__(self) -> str:
return self.title

290
discord/payments.py

@ -0,0 +1,290 @@
"""
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
from typing import TYPE_CHECKING, List, Optional
from .billing import PaymentSource
from .enums import (
PaymentGateway,
PaymentStatus,
SubscriptionType,
try_enum,
)
from .flags import PaymentFlags
from .mixins import Hashable
from .store import SKU
from .subscriptions import Subscription
from .utils import _get_as_snowflake, parse_time
if TYPE_CHECKING:
from .entitlements import Entitlement
from .state import ConnectionState
from .types.payments import (
PartialPayment as PartialPaymentPayload,
Payment as PaymentPayload,
)
__all__ = (
'Payment',
'EntitlementPayment',
)
class Payment(Hashable):
"""Represents a payment to Discord.
.. container:: operations
.. describe:: x == y
Checks if two payments are equal.
.. describe:: x != y
Checks if two payments are not equal.
.. describe:: hash(x)
Returns the payment's hash.
.. describe:: str(x)
Returns the payment's description.
.. versionadded:: 2.0
Attributes
----------
id: :class:`int`
The ID of the payment.
amount: :class:`int`
The amount of the payment.
amount_refunded: :class:`int`
The amount refunded from the payment, if any.
tax: :class:`int`
The amount of tax paid.
tax_inclusive: :class:`bool`
Whether the amount is inclusive of all taxes.
currency: :class:`str`
The currency the payment was made in.
description: :class:`str`
What the payment was for.
status: :class:`PaymentStatus`
The status of the payment.
created_at: :class:`datetime.datetime`
The time the payment was made.
sku: Optional[:class:`SKU`]
The SKU the payment was for, if applicable.
sku_id: Optional[:class:`int`]
The ID of the SKU the payment was for, if applicable.
sku_price: Optional[:class:`int`]
The price of the SKU the payment was for, if applicable.
subscription_plan_id: Optional[:class:`int`]
The ID of the subscription plan the payment was for, if applicable.
subscription: Optional[:class:`Subscription`]
The subscription the payment was for, if applicable.
payment_source: Optional[:class:`PaymentSource`]
The payment source the payment was made with.
payment_gateway: Optional[:class:`PaymentGateway`]
The payment gateway the payment was made with, if applicable.
payment_gateway_payment_id: Optional[:class:`str`]
The ID of the payment on the payment gateway, if any.
invoice_url: Optional[:class:`str`]
The URL to download the VAT invoice for this payment, if available.
refund_invoices_urls: List[:class:`str`]
A list of URLs to download VAT credit notices for refunds on this payment, if available.
refund_disqualification_reasons: List[:class:`str`]
A list of reasons why the payment cannot be refunded, if any.
"""
__slots__ = (
'id',
'amount',
'amount_refunded',
'tax',
'tax_inclusive',
'currency',
'description',
'status',
'created_at',
'sku',
'sku_id',
'sku_price',
'subscription_plan_id',
'subscription',
'payment_source',
'payment_gateway',
'payment_gateway_payment_id',
'invoice_url',
'refund_invoices_urls',
'refund_disqualification_reasons',
'_flags',
'_state',
)
def __init__(self, *, data: PaymentPayload, state: ConnectionState):
self._state: ConnectionState = state
self._update(data)
def _update(self, data: PaymentPayload) -> None:
state = self._state
self.id: int = int(data['id'])
self.amount: int = data['amount']
self.amount_refunded: int = data.get('amount_refunded') or 0
self.tax: int = data.get('tax') or 0
self.tax_inclusive: bool = data.get('tax_inclusive', True)
self.currency: str = data.get('currency', 'usd')
self.description: str = data['description']
self.status: PaymentStatus = try_enum(PaymentStatus, data['status'])
self.created_at: datetime = parse_time(data['created_at'])
self.sku: Optional[SKU] = SKU(data=data['sku'], state=state) if 'sku' in data else None
self.sku_id: Optional[int] = _get_as_snowflake(data, 'sku_id')
self.sku_price: Optional[int] = data.get('sku_price')
self.subscription_plan_id: Optional[int] = _get_as_snowflake(data, 'sku_subscription_plan_id')
self.payment_gateway: Optional[PaymentGateway] = (
try_enum(PaymentGateway, data['payment_gateway']) if 'payment_gateway' in data else None
)
self.payment_gateway_payment_id: Optional[str] = data.get('payment_gateway_payment_id')
self.invoice_url: Optional[str] = data.get('downloadable_invoice')
self.refund_invoices_urls: List[str] = data.get('downloadable_refund_invoices', [])
self.refund_disqualification_reasons: List[str] = data.get('premium_refund_disqualification_reasons', [])
self._flags: int = data.get('flags', 0)
# The subscription object does not include the payment source ID
self.payment_source: Optional[PaymentSource] = (
PaymentSource(data=data['payment_source'], state=state) if 'payment_source' in data else None
)
if 'subscription' in data and self.payment_source:
data['subscription']['payment_source_id'] = self.payment_source.id # type: ignore
self.subscription: Optional[Subscription] = (
Subscription(data=data['subscription'], state=state) if 'subscription' in data else None
)
def __repr__(self) -> str:
return f'<Payment id={self.id} amount={self.amount} currency={self.currency} status={self.status}>'
def __str__(self) -> str:
return self.description
def is_subscription(self) -> bool:
""":class:`bool`: Whether the payment was for a subscription."""
return self.subscription is not None
def is_premium_subscription(self) -> bool:
""":class:`bool`: Whether the payment was for a Discord premium subscription."""
return self.subscription is not None and self.subscription.type == SubscriptionType.premium
def is_premium_subscription_gift(self) -> bool:
""":class:`bool`: Whether the payment was for a Discord premium subscription gift."""
return self.flags.gift and self.sku_id in self._state.premium_subscriptions_sku_ids.values()
def is_purchased_externally(self) -> bool:
""":class:`bool`: Whether the payment was made externally."""
return self.payment_gateway in (PaymentGateway.apple, PaymentGateway.google)
@property
def flags(self) -> PaymentFlags:
""":class:`PaymentFlags`: Returns the payment's flags."""
return PaymentFlags._from_value(self._flags)
async def void(self) -> None:
"""|coro|
Void the payment. Only applicable for payments of status :attr:`PaymentStatus.pending`.
Raises
------
HTTPException
Voiding the payment failed.
"""
await self._state.http.void_payment(self.id)
self.status = PaymentStatus.failed
async def refund(self, reason: Optional[int] = None) -> None:
"""|coro|
Refund the payment.
Raises
------
HTTPException
Refunding the payment failed.
"""
# reason here is an enum (0-8), but I was unable to find the enum values
# Either way, it's optional and this endpoint isn't really used anyway
await self._state.http.refund_payment(self.id, reason)
self.status = PaymentStatus.refunded
class EntitlementPayment(Hashable):
"""Represents a partial payment for an entitlement.
.. container:: operations
.. describe:: x == y
Checks if two payments are equal.
.. describe:: x != y
Checks if two payments are not equal.
.. describe:: hash(x)
Returns the payment's hash.
.. versionadded:: 2.0
Attributes
----------
entitlement: :class:`Entitlement`
The entitlement the payment is for.
id: :class:`int`
The ID of the payment.
amount: :class:`int`
The amount of the payment.
tax: :class:`int`
The amount of tax paid.
tax_inclusive: :class:`bool`
Whether the amount is inclusive of all taxes.
currency: :class:`str`
The currency the payment was made in.
"""
__slots__ = ('entitlement', 'id', 'amount', 'tax', 'tax_inclusive', 'currency')
def __init__(self, *, data: PartialPaymentPayload, entitlement: Entitlement):
self.entitlement = entitlement
self.id: int = int(data['id'])
self.amount: int = data['amount']
self.tax: int = data.get('tax') or 0
self.tax_inclusive: bool = data.get('tax_inclusive', True)
self.currency: str = data.get('currency', 'usd')
def __repr__(self) -> str:
return f'<EntitlementPayment id={self.id} amount={self.amount} currency={self.currency}>'

67
discord/profile.py

@ -116,8 +116,6 @@ class Profile:
class ApplicationProfile(Hashable):
"""Represents a Discord application profile.
.. versionadded:: 2.0
.. container:: operations
.. describe:: x == y
@ -132,6 +130,8 @@ class ApplicationProfile(Hashable):
Return the applications's hash.
.. versionadded:: 2.0
Attributes
------------
id: :class:`int`
@ -142,6 +142,7 @@ class ApplicationProfile(Hashable):
A list of the IDs of the application's popular commands.
primary_sku_id: Optional[:class:`int`]
The application's primary SKU ID, if any.
This can be an application's game SKU, subscription SKU, etc.
custom_install_url: Optional[:class:`str`]
The custom URL to use for authorizing the application, if specified.
install_params: Optional[:class:`ApplicationInstallParams`]
@ -158,7 +159,7 @@ class ApplicationProfile(Hashable):
params = data.get('install_params')
self.custom_install_url: Optional[str] = data.get('custom_install_url')
self.install_params: Optional[ApplicationInstallParams] = (
ApplicationInstallParams(self.id, params) if params else None
ApplicationInstallParams.from_application(self, params) if params else None
)
def __repr__(self) -> str:
@ -174,9 +175,37 @@ class ApplicationProfile(Hashable):
""":class:`str`: The URL to install the application."""
return self.custom_install_url or self.install_params.url if self.install_params else None
@property
def primary_sku_url(self) -> Optional[str]:
""":class:`str`: The URL to the primary SKU of the application, if any."""
if self.primary_sku_id:
return f'https://discord.com/store/skus/{self.primary_sku_id}/unknown'
class UserProfile(Profile, User):
"""Represents a Discord user's profile. This is a :class:`User` with extended attributes.
"""Represents a Discord user's profile.
This is a :class:`User` with extended attributes.
.. container:: operations
.. describe:: x == y
Checks if two users are equal.
.. describe:: x != y
Checks if two users are not equal.
.. describe:: hash(x)
Return the user's hash.
.. describe:: str(x)
Returns the user's name with discriminator.
.. versionadded:: 2.0
Attributes
-----------
@ -208,7 +237,31 @@ class UserProfile(Profile, User):
class MemberProfile(Profile, Member):
"""Represents a Discord member's profile. This is a :class:`Member` with extended attributes.
"""Represents a Discord member's profile.
This is a :class:`Member` with extended attributes.
.. container:: operations
.. describe:: x == y
Checks if two members are equal.
Note that this works with :class:`User` instances too.
.. describe:: x != y
Checks if two members are not equal.
Note that this works with :class:`User` instances too.
.. describe:: hash(x)
Returns the member's hash.
.. describe:: str(x)
Returns the member's name with the discriminator.
.. versionadded:: 2.0
Attributes
-----------
@ -225,13 +278,13 @@ class MemberProfile(Profile, Member):
.. note::
This is renamed from :attr:`Member.premium_since` because of name collisions.
premium_type: Optional[:class:`PremiumType`]
Specifies the type of premium a user has (i.e. Nitro, Nitro Classic, or Nitro Basic). Could be None if the user is not premium.
Specifies the type of premium a user has (i.e. Nitro, Nitro Classic, or Nitro Basic). Could be ``None`` if the user is not premium.
premium_since: Optional[:class:`datetime.datetime`]
An aware datetime object that specifies how long a user has been premium (had Nitro).
``None`` if the user is not a premium user.
.. note::
This is not the same as :attr:`Member.premium_since`. That is renamed to :attr:`guild_premium_since`
This is not the same as :attr:`Member.premium_since`. That is renamed to :attr:`guild_premium_since`.
boosting_since: Optional[:class:`datetime.datetime`]
An aware datetime object that specifies when a user first boosted any guild.
connections: Optional[List[:class:`PartialConnection`]]

306
discord/promotions.py

@ -0,0 +1,306 @@
"""
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
from typing import TYPE_CHECKING, List, Optional, Union
from .enums import PaymentSourceType, try_enum
from .flags import PromotionFlags
from .mixins import Hashable
from .subscriptions import SubscriptionTrial
from .utils import _get_as_snowflake, parse_time, utcnow
if TYPE_CHECKING:
from .state import ConnectionState
from .types.promotions import (
ClaimedPromotion as ClaimedPromotionPayload,
Promotion as PromotionPayload,
TrialOffer as TrialOfferPayload,
PricingPromotion as PricingPromotionPayload,
)
__all__ = (
'Promotion',
'TrialOffer',
'PricingPromotion',
)
class Promotion(Hashable):
"""Represents a Discord promotion.
.. container:: operations
.. describe:: x == y
Checks if two promotions are equal.
.. describe:: x != y
Checks if two promotions are not equal.
.. describe:: hash(x)
Returns the promotion's hash.
.. describe:: str(x)
Returns the outbound promotion's name.
.. versionadded:: 2.0
Attributes
----------
id: :class:`int`
The promotion ID.
trial_id: Optional[:class:`int`]
The trial ID of the inbound promotion, if applicable.
starts_at: :class:`datetime.datetime`
When the promotion starts.
ends_at: :class:`datetime.datetime`
When the promotion ends.
claimed_at: Optional[:class:`datetime.datetime`]
When the promotion was claimed.
Only available for claimed promotions.
code: Optional[:class:`str`]
The promotion's claim code. Only available for claimed promotions.
outbound_title: :class:`str`
The title of the outbound promotion.
outbound_description: :class:`str`
The description of the outbound promotion.
outbound_link: :class:`str`
The redemption page of the outbound promotion, used to claim it.
outbound_restricted_countries: List[:class:`str`]
The countries that the outbound promotion is not available in.
inbound_title: Optional[:class:`str`]
The title of the inbound promotion. This is usually Discord Nitro.
inbound_description: Optional[:class:`str`]
The description of the inbound promotion.
inbound_link: Optional[:class:`str`]
The Discord help center link of the inbound promotion.
inbound_restricted_countries: List[:class:`str`]
The countries that the inbound promotion is not available in.
terms_and_conditions: :class:`str`
The terms and conditions of the promotion.
"""
__slots__ = (
'id',
'trial_id',
'starts_at',
'ends_at',
'claimed_at',
'code',
'outbound_title',
'outbound_description',
'outbound_link',
'outbound_restricted_countries',
'inbound_title',
'inbound_description',
'inbound_link',
'inbound_restricted_countries',
'terms_and_conditions',
'_flags',
'_state',
)
def __init__(self, *, data: Union[PromotionPayload, ClaimedPromotionPayload], state: ConnectionState) -> None:
self._state = state
self._update(data)
def __str__(self) -> str:
return self.outbound_title
def __repr__(self) -> str:
return f'<Promotion id={self.id} title={self.outbound_title!r}>'
def _update(self, data: Union[PromotionPayload, ClaimedPromotionPayload]) -> None:
promotion: PromotionPayload = data.get('promotion', data) # type: ignore
self.id: int = int(promotion['id'])
self.trial_id: Optional[int] = _get_as_snowflake(promotion, 'trial_id')
self.starts_at: datetime = parse_time(promotion['start_date'])
self.ends_at: datetime = parse_time(promotion['end_date'])
self.claimed_at: Optional[datetime] = parse_time(data.get('claimed_at'))
self.code: Optional[str] = data.get('code')
self._flags: int = promotion.get('flags', 0)
self.outbound_title: str = promotion['outbound_title']
self.outbound_description: str = promotion['outbound_redemption_modal_body']
self.outbound_link: str = promotion.get(
'outbound_redemption_page_link',
promotion.get('outbound_redemption_url_format', '').replace('{code}', self.code or '{code}'),
)
self.outbound_restricted_countries: List[str] = promotion.get('outbound_restricted_countries', [])
self.inbound_title: Optional[str] = promotion.get('inbound_header_text')
self.inbound_description: Optional[str] = promotion.get('inbound_body_text')
self.inbound_link: Optional[str] = promotion.get('inbound_help_center_link')
self.inbound_restricted_countries: List[str] = promotion.get('inbound_restricted_countries', [])
self.terms_and_conditions: str = promotion['outbound_terms_and_conditions']
@property
def flags(self) -> PromotionFlags:
""":class:`PromotionFlags`: Returns the promotion's flags."""
return PromotionFlags._from_value(self._flags)
def is_claimed(self) -> bool:
""":class:`bool`: Checks if the promotion has been claimed.
Only accurate if the promotion was fetched from :meth:`Client.promotions` with ``claimed`` set to ``True`` or :meth:`claim` was just called.
"""
return self.claimed_at is not None
def is_active(self) -> bool:
""":class:`bool`: Checks if the promotion is active."""
return self.starts_at <= utcnow() <= self.ends_at
async def claim(self) -> str:
"""|coro|
Claims the promotion.
Sets :attr:`claimed_at` and :attr:`code`.
Raises
------
Forbidden
You are not allowed to claim the promotion.
HTTPException
Claiming the promotion failed.
Returns
-------
:class:`str`
The claim code for the outbound promotion.
"""
data = await self._state.http.claim_promotion(self.id)
self._update(data)
return data['code']
class TrialOffer(Hashable):
"""Represents a Discord user trial offer.
.. container:: operations
.. describe:: x == y
Checks if two trial offers are equal.
.. describe:: x != y
Checks if two trial offers are not equal.
.. describe:: hash(x)
Returns the trial offer's hash.
.. versionadded:: 2.0
Attributes
----------
id: :class:`int`
The ID of the trial offer.
expires_at: :class:`datetime.datetime`
When the trial offer expires.
trial_id: :class:`int`
The ID of the trial.
trial: :class:`SubscriptionTrial`
The trial offered.
"""
__slots__ = (
'id',
'expires_at',
'trial_id',
'trial',
'_state',
)
def __init__(self, *, data: TrialOfferPayload, state: ConnectionState) -> None:
self._state = state
self.id: int = int(data['id'])
self.expires_at: datetime = parse_time(data['expires_at'])
self.trial_id: int = int(data['trial_id'])
self.trial: SubscriptionTrial = SubscriptionTrial(data['subscription_trial'])
def __repr__(self) -> str:
return f'<TrialOffer id={self.id} trial={self.trial!r}>'
async def ack(self) -> None:
"""|coro|
Acknowledges the trial offer.
Raises
------
HTTPException
Acknowledging the trial offer failed.
"""
await self._state.http.ack_trial_offer(self.id)
class PricingPromotion:
"""Represents a Discord localized pricing promotion.
.. versionadded:: 2.0
Attributes
----------
subscription_plan_id: :class:`int`
The ID of the subscription plan the promotion is for.
country_code: :class:`str`
The country code the promotion applies to.
payment_source_types: List[:class:`PaymentSourceType`]
The payment source types the promotion is restricted to.
amount: :class:`int`
The discounted price of the subscription plan.
currency: :class:`str`
The currency of the discounted price.
"""
__slots__ = (
'subscription_plan_id',
'country_code',
'payment_source_types',
'amount',
'currency',
)
def __init__(self, *, data: PricingPromotionPayload) -> None:
self.subscription_plan_id: int = int(data['plan_id'])
self.country_code: str = data['country_code']
self.payment_source_types: List[PaymentSourceType] = [
try_enum(PaymentSourceType, t) for t in data['payment_source_types']
]
price = data['price']
self.amount: int = price['amount']
self.currency: str = price['currency']
def __repr__(self) -> str:
return f'<PricingPromotion plan_id={self.subscription_plan_id} country_code={self.country_code!r} amount={self.amount} currency={self.currency!r}>'

2
discord/role.py

@ -460,7 +460,7 @@ class Role(Hashable):
data = await state.http.add_members_to_role(guild.id, self.id, [m.id for m in members], reason=reason)
return [Member(data=m, state=state, guild=guild) for m in data.values()]
async def remove_roles(self, *members: Snowflake, reason: Optional[str] = None) -> None:
async def remove_members(self, *members: Snowflake, reason: Optional[str] = None) -> None:
r"""|coro|
Removes :class:`Member`\s from this role.

156
discord/state.py

@ -62,7 +62,7 @@ from .raw_models import *
from .member import Member
from .relationship import Relationship
from .role import Role
from .enums import ChannelType, RequiredActionType, Status, try_enum, UnavailableGuildType
from .enums import ChannelType, PaymentSourceType, RequiredActionType, Status, try_enum, UnavailableGuildType
from . import utils
from .flags import MemberCacheFlags
from .invite import Invite
@ -77,8 +77,12 @@ from .permissions import Permissions, PermissionOverwrite
from .member import _ClientStatus
from .modal import Modal
from .member import VoiceState
from .appinfo import InteractionApplication
from .appinfo import IntegrationApplication, PartialApplication, Achievement
from .connections import Connection
from .payments import Payment
from .entitlements import Entitlement, Gift
from .guild_premium import PremiumGuildSubscriptionSlot
from .library import LibraryApplication
if TYPE_CHECKING:
from .abc import PrivateChannel, Snowflake as abcSnowflake
@ -92,6 +96,7 @@ if TYPE_CHECKING:
from .types.snowflake import Snowflake
from .types.activity import Activity as ActivityPayload
from .types.appinfo import Achievement as AchievementPayload, IntegrationApplication as IntegrationApplicationPayload
from .types.channel import DMChannel as DMChannelPayload
from .types.user import User as UserPayload, PartialUser as PartialUserPayload
from .types.emoji import Emoji as EmojiPayload, PartialEmoji as PartialEmojiPayload
@ -462,10 +467,12 @@ class ConnectionState:
self.guild_settings: Dict[Optional[int], GuildSettings] = {}
self.consents: Optional[TrackingSettings] = None
self.connections: Dict[str, Connection] = {}
self.pending_payments: Dict[int, Payment] = {}
self.analytics_token: Optional[str] = None
self.preferred_regions: List[str] = []
self.country_code: Optional[str] = None
self.session_type: Optional[str] = None
self.auth_session_id: Optional[str] = None
self._emojis: Dict[int, Emoji] = {}
self._stickers: Dict[int, GuildSticker] = {}
self._guilds: Dict[int, Guild] = {}
@ -533,6 +540,10 @@ class ConnectionState:
u = self.user
return u.id if u else None
@property
def locale(self) -> str:
return str(getattr(self.user, 'locale', 'en-US'))
@property
def preferred_region(self) -> str:
return self.preferred_regions[0] if self.preferred_regions else 'us-central'
@ -607,6 +618,9 @@ class ConnectionState:
return user
def create_user(self, data: Union[UserPayload, PartialUserPayload]) -> User:
user_id = int(data['id'])
if user_id == self.self_id:
return self.user # type: ignore
return User(state=self, data=data)
def get_user(self, id: int) -> Optional[User]:
@ -819,7 +833,7 @@ class ConnectionState:
data = self._ready_data
# Temp user parsing
temp_users: Dict[int, UserPayload] = {int(data['user']['id']): data['user']}
temp_users: Dict[int, PartialUserPayload] = {int(data['user']['id']): data['user']}
for u in data.get('users', []):
u_id = int(u['id'])
temp_users[u_id] = u
@ -893,7 +907,9 @@ class ConnectionState:
self.consents = TrackingSettings(data=data.get('consents', {}), state=self)
self.country_code = data.get('country_code', 'US')
self.session_type = data.get('session_type', 'normal')
self.auth_session_id = data.get('auth_session_id_hash')
self.connections = {c['id']: Connection(state=self, data=c) for c in data.get('connected_accounts', [])}
self.pending_payments = {int(p['id']): Payment(state=self, data=p) for p in data.get('pending_payments', [])}
if 'required_action' in data:
self.parse_user_required_action_update(data)
@ -1054,7 +1070,7 @@ class ConnectionState:
member_id = int(user['id'])
member = guild.get_member(member_id)
if member is None:
_log.debug('PRESENCE_UPDATE referencing an unknown member ID: %s. Discarding', member_id)
_log.debug('PRESENCE_UPDATE referencing an unknown member ID: %s. Discarding.', member_id)
return
old_member = Member._copy(member)
@ -1088,12 +1104,17 @@ class ConnectionState:
settings = GuildSettings(data=data, state=self)
self.dispatch('guild_settings_update', old_settings, settings)
def parse_user_required_action_update(self, data) -> None:
required_action = try_enum(RequiredActionType, data['required_action'])
def parse_user_required_action_update(self, data: Union[gw.RequiredActionEvent, gw.ReadyEvent]) -> None:
required_action = try_enum(RequiredActionType, data['required_action']) # type: ignore
self.dispatch('required_action_update', required_action)
def parse_user_connections_update(self, data: gw.Connection) -> None:
id = data['id']
def parse_user_connections_update(self, data: Union[gw.ConnectionEvent, gw.PartialConnectionEvent]) -> None:
self.dispatch('connections_update')
id = data.get('id')
if id is None or 'user_id' in data:
return
if id not in self.connections:
self.connections[id] = connection = Connection(state=self, data=data)
self.dispatch('connection_create', connection)
@ -1107,6 +1128,69 @@ class ConnectionState:
connection._update(data)
self.dispatch('connection_update', old_connection, connection)
def parse_user_connections_link_callback(self, data: gw.ConnectionsLinkCallbackEvent) -> None:
self.dispatch('connections_link_callback', data['provider'], data['callback_code'], data['callback_state'])
def parse_user_payment_sources_update(self, data: gw.NoEvent) -> None:
self.dispatch('payment_sources_update')
def parse_user_subscriptions_update(self, data: gw.NoEvent) -> None:
self.dispatch('subscriptions_update')
def parse_user_payment_client_add(self, data: gw.PaymentClientAddEvent) -> None:
self.dispatch('payment_client_add', data['purchase_token_hash'], utils.parse_time(data['expires_at']))
def parse_user_premium_guild_subscription_slot_create(self, data: gw.PremiumGuildSubscriptionSlotEvent) -> None:
slot = PremiumGuildSubscriptionSlot(state=self, data=data)
self.dispatch('premium_guild_subscription_slot_create', slot)
def parse_user_premium_guild_subscription_slot_update(self, data: gw.PremiumGuildSubscriptionSlotEvent) -> None:
slot = PremiumGuildSubscriptionSlot(state=self, data=data)
self.dispatch('premium_guild_subscription_slot_update', slot)
def parse_user_achievement_update(self, data: gw.AchievementUpdatePayload) -> None:
achievement: AchievementPayload = data.get('achievement') # type: ignore
application_id = data.get('application_id')
if not achievement or not application_id:
_log.warning('USER_ACHIEVEMENT_UPDATE payload has invalid data: %s. Discarding.', list(data.keys()))
return
achievement['application_id'] = application_id
model = Achievement(state=self, data=achievement)
self.dispatch('achievement_update', model, data.get('percent_complete', 0))
def parse_billing_popup_bridge_callback(self, data: gw.BillingPopupBridgeCallbackEvent) -> None:
self.dispatch(
'billing_popup_bridge_callback',
try_enum(PaymentSourceType, data.get('payment_source_type', 0)),
data.get('path'),
data.get('query'),
data.get('state'),
)
def parse_oauth2_token_revoke(self, data: gw.OAuth2TokenRevokeEvent) -> None:
if 'access_token' not in data:
_log.warning('OAUTH2_TOKEN_REVOKE payload has invalid data: %s. Discarding.', list(data.keys()))
self.dispatch('oauth2_token_revoke', data['access_token'])
def parse_auth_session_change(self, data: gw.AuthSessionChangeEvent) -> None:
self.auth_session_id = auth_session_id = data['auth_session_id_hash']
self.dispatch('auth_session_change', auth_session_id)
def parse_payment_update(self, data: gw.PaymentUpdateEvent) -> None:
id = int(data['id'])
payment = self.pending_payments.get(id)
if payment is not None:
payment._update(data)
else:
payment = Payment(state=self, data=data)
self.dispatch('payment_update', payment)
def parse_library_application_update(self, data: gw.LibraryApplicationUpdateEvent) -> None:
entry = LibraryApplication(state=self, data=data)
self.dispatch('library_application_update', entry)
def parse_sessions_replace(self, data: List[Dict[str, Any]]) -> None:
overall = MISSING
this = MISSING
@ -1148,6 +1232,28 @@ class ConnectionState:
client._client_activities = client_activities
client._session_count = len(data)
def parse_entitlement_create(self, data: gw.EntitlementEvent) -> None:
entitlement = Entitlement(state=self, data=data)
self.dispatch('entitlement_create', entitlement)
def parse_entitlement_update(self, data: gw.EntitlementEvent) -> None:
entitlement = Entitlement(state=self, data=data)
self.dispatch('entitlement_update', entitlement)
def parse_entitlement_delete(self, data: gw.EntitlementEvent) -> None:
entitlement = Entitlement(state=self, data=data)
self.dispatch('entitlement_delete', entitlement)
def parse_gift_code_create(self, data: gw.GiftCreateEvent) -> None:
# Should be fine:tm:
gift = Gift(state=self, data=data) # type: ignore
self.dispatch('gift_create', gift)
def parse_gift_code_update(self, data: gw.GiftUpdateEvent) -> None:
# Should be fine:tm:
gift = Gift(state=self, data=data) # type: ignore
self.dispatch('gift_update', gift)
def parse_invite_create(self, data: gw.InviteCreateEvent) -> None:
invite = Invite.from_gateway(state=self, data=data)
self.dispatch('invite_create', invite)
@ -1642,6 +1748,8 @@ class ConnectionState:
guild.command_counts = CommandCounts(data.get(0, 0), data.get(1, 0), data.get(2, 0))
parse_guild_application_command_index_update = parse_guild_application_command_counts_update
def parse_guild_emojis_update(self, data: gw.GuildEmojisUpdateEvent) -> None:
guild = self._get_guild(int(data['guild_id']))
if guild is None:
@ -1670,7 +1778,7 @@ class ConnectionState:
def _get_create_guild(self, data: gw.GuildCreateEvent):
guild = self._get_guild(int(data['id']))
# Discord being Discord sends a GUILD_CREATE after an OPCode 14 is sent (a la bots)
# Discord being Discord sometimes sends a GUILD_CREATE after an OPCode 14 is sent (a la bots)
# However, we want that if we forced a GUILD_CREATE for an unavailable guild
if guild is not None:
guild._from_data(data)
@ -2310,11 +2418,37 @@ class ConnectionState:
def create_message(self, *, channel: MessageableChannel, data: MessagePayload) -> Message:
return Message(state=self, channel=channel, data=data)
def create_interaction_application(self, data: dict) -> InteractionApplication:
return InteractionApplication(state=self, data=data)
def create_integration_application(self, data: IntegrationApplicationPayload) -> IntegrationApplication:
return IntegrationApplication(state=self, data=data)
def default_guild_settings(self, guild_id: Optional[int]) -> GuildSettings:
return GuildSettings(data={'guild_id': guild_id}, state=self)
def default_channel_settings(self, guild_id: Optional[int], channel_id: int) -> ChannelSettings:
return ChannelSettings(guild_id, data={'channel_id': channel_id}, state=self)
@utils.cached_property
def premium_subscriptions_application(self) -> PartialApplication:
# Hardcoded application for premium subscriptions, highly unlikely to change
return PartialApplication(
state=self,
data={
'id': 521842831262875670,
'name': 'Nitro',
'icon': None,
'description': '',
'verify_key': '93661a9eefe452d12f51e129e8d9340e7ca53a770158c0ec7970e701534b7420',
'type': None,
},
)
@utils.cached_property
def premium_subscriptions_sku_ids(self) -> Dict[str, Snowflake]:
return {
'none': 628379670982688768,
'basic': 978380684370378762,
'legacy': 521842865731534868,
'classic': 521846918637420545,
'full': 521847234246082599,
'guild': 590663762298667008,
}

2281
discord/store.py

File diff suppressed because it is too large

859
discord/subscriptions.py

@ -0,0 +1,859 @@
"""
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, Dict, List, Optional, Union
from .billing import PaymentSource
from .enums import (
PaymentGateway,
SubscriptionDiscountType,
SubscriptionInterval,
SubscriptionInvoiceStatus,
SubscriptionStatus,
SubscriptionType,
try_enum,
)
from .metadata import Metadata
from .mixins import Hashable
from .utils import MISSING, _get_as_snowflake, parse_time, snowflake_time, utcnow
if TYPE_CHECKING:
from typing_extensions import Self
from .abc import Snowflake
from .guild import Guild
from .state import ConnectionState
from .types.subscriptions import (
SubscriptionDiscount as SubscriptionDiscountPayload,
SubscriptionInvoice as SubscriptionInvoicePayload,
SubscriptionInvoiceItem as SubscriptionInvoiceItemPayload,
SubscriptionItem as SubscriptionItemPayload,
SubscriptionRenewalMutations as SubscriptionRenewalMutationsPayload,
PartialSubscription as PartialSubscriptionPayload,
Subscription as SubscriptionPayload,
SubscriptionTrial as SubscriptionTrialPayload,
)
__all__ = (
'SubscriptionItem',
'SubscriptionDiscount',
'SubscriptionInvoiceItem',
'SubscriptionInvoice',
'SubscriptionRenewalMutations',
'Subscription',
'SubscriptionTrial',
)
class SubscriptionItem(Hashable):
"""Represents a Discord subscription item.
.. container:: operations
.. describe:: x == y
Checks if two subscription items are equal.
.. describe:: x != y
Checks if two subscription items are not equal.
.. describe:: hash(x)
Returns the item's hash.
.. describe:: len(x)
Returns the quantity of the subscription item.
.. versionadded:: 2.0
Attributes
----------
id: Optional[:class:`int`]
The ID of the subscription item. Always available when received from the API.
quantity: :class:`int`
How many of the item have been/are being purchased.
plan_id: :class:`int`
The ID of the plan the item is for.
"""
__slots__ = ('id', 'quantity', 'plan_id')
def __init__(self, *, id: Optional[int] = None, plan_id: int, quantity: int = 1) -> None:
self.id: Optional[int] = id
self.quantity: int = quantity
self.plan_id: int = plan_id
def __repr__(self) -> str:
return f'<SubscriptionItem {f"id={self.id} " if self.id else ""}plan_id={self.plan_id} quantity={self.quantity}>'
def __len__(self) -> int:
return self.quantity
@classmethod
def from_dict(cls, data: SubscriptionItemPayload) -> Self:
return cls(id=int(data['id']), plan_id=int(data['plan_id']), quantity=int(data.get('quantity', 1)))
def to_dict(self, with_id: bool = True) -> dict:
data = {
'quantity': self.quantity,
'plan_id': self.plan_id,
}
if self.id and with_id:
data['id'] = self.id
return data
class SubscriptionDiscount:
"""Represents a discount on a Discord subscription item.
.. container:: operations
.. describe:: int(x)
Returns the discount's amount.
.. versionadded:: 2.0
Attributes
----------
type: :class:`SubscriptionDiscountType`
The type of the discount.
amount: :class:`int`
How much the discount is.
"""
__slots__ = ('type', 'amount')
def __init__(self, data: SubscriptionDiscountPayload) -> None:
self.type: SubscriptionDiscountType = try_enum(SubscriptionDiscountType, data['type'])
self.amount: int = data['amount']
def __repr__(self) -> str:
return f'<SubscriptionDiscount type={self.type!r} amount={self.amount}>'
def __int__(self) -> int:
return self.amount
class SubscriptionInvoiceItem(Hashable):
"""Represents an invoice item.
.. container:: operations
.. describe:: x == y
Checks if two invoice items are equal.
.. describe:: x != y
Checks if two invoice items are not equal.
.. describe:: hash(x)
Returns the invoice's hash.
.. describe:: len(x)
Returns the quantity of the invoice item.
.. versionadded:: 2.0
Attributes
----------
id: :class:`int`
The ID of the invoice item.
quantity: :class:`int`
How many of the item have been/are being purchased.
amount: :class:`int`
The price of the item. This includes discounts.
proration: :class:`bool`
Whether the item is prorated.
plan_id: :class:`int`
The ID of the subscription plan the item represents.
plan_price: :class:`int`
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.
"""
__slots__ = ('id', 'quantity', 'amount', 'proration', 'plan_id', 'plan_price', 'discounts')
def __init__(self, data: SubscriptionInvoiceItemPayload) -> None:
self.id: int = int(data['id'])
self.quantity: int = data['quantity']
self.amount: int = data['amount']
self.proration: bool = data.get('proration', False)
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']]
def __repr__(self) -> str:
return f'<SubscriptionInvoiceItem id={self.id} quantity={self.quantity} amount={self.amount}>'
def __len__(self) -> int:
return self.quantity
@property
def savings(self) -> int:
""":class:`int`: The total amount of discounts on the invoice item."""
return self.plan_price - self.amount
def is_discounted(self) -> bool:
""":class:`bool`: Indicates if the invoice item has a discount."""
return bool(self.discounts)
def is_trial(self) -> bool:
""":class:`bool`: Indicates if the invoice item is a trial."""
return not self.amount or any(discount.type is SubscriptionDiscountType.premium_trial for discount in self.discounts)
class SubscriptionInvoice(Hashable):
"""Represents an invoice for a Discord subscription.
.. container:: operations
.. describe:: x == y
Checks if two invoices are equal.
.. describe:: x != y
Checks if two invoices are not equal.
.. describe:: hash(x)
Returns the invoice's hash.
.. versionadded:: 2.0
Attributes
----------
subscription: Optional[:class:`Subscription`]
The subscription the invoice is for. Not available for new subscription previews.
id: :class:`int`
The ID of the invoice.
status: Optional[:class:`SubscriptionInvoiceStatus`]
The status of the invoice. Not available for subscription previews.
currency: :class:`str`
The currency the invoice is in.
subtotal: :class:`int`
The subtotal of the invoice.
tax: :class:`int`
The tax applied to the invoice.
total: :class:`int`
The total of the invoice.
tax_inclusive: :class:`bool`
Whether the subtotal is inclusive of all taxes.
items: List[:class:`SubscriptionInvoiceItem`]
The items in the invoice.
current_period_start: :class:`datetime.datetime`
When the current billing period started.
current_period_end: :class:`datetime.datetime`
When the current billing period ends.
"""
__slots__ = (
'_state',
'subscription',
'id',
'status',
'currency',
'subtotal',
'tax',
'total',
'tax_inclusive',
'items',
'current_period_start',
'current_period_end',
)
def __init__(
self, subscription: Optional[Subscription], *, data: SubscriptionInvoicePayload, state: ConnectionState
) -> None:
self._state = state
self.subscription = subscription
self._update(data)
def _update(self, data: SubscriptionInvoicePayload) -> None:
self.id: int = int(data['id'])
self.status: Optional[SubscriptionInvoiceStatus] = (
try_enum(SubscriptionInvoiceStatus, data['status']) if 'status' in data else None
)
self.currency: str = data['currency']
self.subtotal: int = data['subtotal']
self.tax: int = data.get('tax', 0)
self.total: int = data['total']
self.tax_inclusive: bool = data['tax_inclusive']
self.items: List[SubscriptionInvoiceItem] = [SubscriptionInvoiceItem(d) for d in data.get('invoice_items', [])]
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
def __repr__(self) -> str:
return f'<SubscriptionInvoice id={self.id} status={self.status!r} total={self.total}>'
def is_discounted(self) -> bool:
""":class:`bool`: Indicates if the invoice has a discount."""
return any(item.discounts for item in self.items)
def is_preview(self) -> bool:
""":class:`bool`: Indicates if the invoice is a preview and not real."""
return self.subscription is None or self.status is None
async def pay(
self,
payment_source: Optional[Snowflake] = None,
currency: str = 'usd',
*,
payment_source_token: Optional[str] = None,
return_url: Optional[str] = None,
) -> None:
"""|coro|
Pays the invoice.
Parameters
----------
payment_source: Optional[:class:`PaymentSource`]
The payment source the invoice should be paid with.
currency: :class:`str`
The currency to pay with.
payment_source_token: Optional[:class:`str`]
The token used to authorize with the payment source.
return_url: Optional[:class:`str`]
The URL to return to after the payment is complete.
Raises
------
TypeError
The invoice is a preview and not real.
NotFound
The invoice is not open or found.
HTTPException
Paying the invoice failed.
"""
if self.is_preview() or not self.subscription:
raise TypeError('Cannot pay a nonexistant invoice')
data = await self._state.http.pay_invoice(
self.subscription.id,
self.id,
payment_source.id if payment_source else None,
payment_source_token,
currency,
return_url,
)
self.subscription._update(data)
class SubscriptionRenewalMutations:
"""Represents a subscription renewal mutation.
This represents changes to a subscription that will occur after renewal.
.. container:: operations
.. describe:: len(x)
Returns the number of items in the changed subscription, including quantity.
.. describe:: bool(x)
Returns whether any mutations are present.
.. versionadded:: 2.0
Attributes
----------
payment_gateway_plan_id: Optional[:class:`str`]
The payment gateway's new plan ID for the subscription.
This signifies an external plan change.
items: Optional[List[:class:`SubscriptionItem`]]
The new items of the subscription.
"""
__slots__ = ('payment_gateway_plan_id', 'items')
def __init__(self, data: SubscriptionRenewalMutationsPayload) -> None:
self.payment_gateway_plan_id: Optional[str] = data.get('payment_gateway_plan_id')
self.items: Optional[List[SubscriptionItem]] = (
[SubscriptionItem.from_dict(item) for item in data['items']] if 'items' in data else None
)
def __repr__(self) -> str:
return (
f'<SubscriptionRenewalMutations payment_gateway_plan_id={self.payment_gateway_plan_id!r} items={self.items!r}>'
)
def __len__(self) -> int:
return sum(item.quantity for item in self.items) if self.items else 0
def __bool__(self) -> bool:
return self.is_mutated()
def is_mutated(self) -> bool:
""":class:`bool`: Checks if any renewal mutations exist."""
return self.payment_gateway_plan_id is not None or self.items is not None
class Subscription(Hashable):
"""Represents a Discord subscription.
.. container:: operations
.. describe:: x == y
Checks if two premium subscriptions are equal.
.. describe:: x != y
Checks if two premium subscriptions are not equal.
.. describe:: hash(x)
Returns the subscription's hash.
.. describe:: len(x)
Returns the number of items in the subscription, including quantity.
.. describe:: bool(x)
Checks if the subscription is currently active and offering perks.
.. versionadded:: 2.0
Attributes
----------
id: :class:`int`
The ID of the subscription.
type: :class:`SubscriptionType`
The type of the subscription.
status: Optional[:class:`SubscriptionStatus`]
The status of the subscription. This is ``None`` for fake subscriptions.
payment_gateway: Optional[:class:`PaymentGateway`]
The payment gateway used to bill the subscription.
currency: :class:`str`
The currency the subscription is billed in.
items: List[:class:`SubscriptionItem`]
The items in the subscription.
renewal_mutations: :class:`SubscriptionRenewalMutations`
The mutations to the subscription that will occur after renewal.
trial_id: Optional[:class:`int`]
The ID of the trial the subscription is from, if applicable.
payment_source_id: Optional[:class:`int`]
The ID of the payment source the subscription is paid with, if applicable.
payment_gateway_plan_id: Optional[:class:`str`]
The payment gateway's plan ID for the subscription, if applicable.
payment_gateway_subscription_id: Optional[:class:`str`]
The payment gateway's subscription ID for the subscription, if applicable.
created_at: :class:`datetime.datetime`
When the subscription was created.
canceled_at: Optional[:class:`datetime.datetime`]
When the subscription was canceled.
This is only available for subscriptions with a :attr:`status` of :attr:`SubscriptionStatus.canceled`.
current_period_start: :class:`datetime.datetime`
When the current billing period started.
current_period_end: :class:`datetime.datetime`
When the current billing period ends.
trial_ends_at: Optional[:class:`datetime.datetime`]
When the trial ends, if applicable.
streak_started_at: Optional[:class:`datetime.datetime`]
When the current subscription streak started.
ended_at: Optional[:class:`datetime.datetime`]
When the subscription finally ended.
metadata: :class:`Metadata`
Extra metadata about the subscription.
latest_invoice: Optional[:class:`SubscriptionInvoice`]
The latest invoice for the subscription, if applicable.
"""
__slots__ = (
'_state',
'id',
'type',
'status',
'payment_gateway',
'currency',
'items',
'renewal_mutations',
'trial_id',
'payment_source_id',
'payment_gateway_plan_id',
'payment_gateway_subscription_id',
'created_at',
'canceled_at',
'current_period_start',
'current_period_end',
'trial_ends_at',
'streak_started_at',
'ended_at',
'metadata',
'latest_invoice',
)
def __init__(self, *, data: Union[PartialSubscriptionPayload, SubscriptionPayload], state: ConnectionState) -> None:
self._state = state
self._update(data)
def __repr__(self) -> str:
return f'<Subscription id={self.id} currency={self.currency!r} items={self.items!r}>'
def __len__(self) -> int:
return sum(item.quantity for item in self.items)
def __bool__(self) -> bool:
return self.is_active()
def _update(self, data: PartialSubscriptionPayload) -> None:
self.id: int = int(data['id'])
self.type: SubscriptionType = try_enum(SubscriptionType, data['type'])
self.status: Optional[SubscriptionStatus] = (
try_enum(SubscriptionStatus, data['status']) if 'status' in data else None # type: ignore # ???
)
self.payment_gateway: Optional[PaymentGateway] = (
try_enum(PaymentGateway, data['payment_gateway']) if 'payment_gateway' in data else None
)
self.currency: str = data.get('currency', 'usd')
self.items: List[SubscriptionItem] = [SubscriptionItem.from_dict(item) for item in data.get('items', [])]
self.renewal_mutations: SubscriptionRenewalMutations = SubscriptionRenewalMutations(
data.get('renewal_mutations') or {}
)
self.trial_id: Optional[int] = _get_as_snowflake(data, 'trial_id')
self.payment_source_id: Optional[int] = _get_as_snowflake(data, 'payment_source_id')
self.payment_gateway_plan_id: Optional[str] = data.get('payment_gateway_plan_id')
self.payment_gateway_subscription_id: Optional[str] = data.get('payment_gateway_subscription_id')
self.created_at: datetime = parse_time(data.get('created_at')) or snowflake_time(self.id)
self.canceled_at: Optional[datetime] = parse_time(data.get('canceled_at'))
self.current_period_start: datetime = parse_time(data['current_period_start'])
self.current_period_end: datetime = parse_time(data['current_period_end'])
self.trial_ends_at: Optional[datetime] = parse_time(data.get('trial_ends_at'))
self.streak_started_at: Optional[datetime] = parse_time(data.get('streak_started_at'))
metadata = data.get('metadata') or {}
self.ended_at: Optional[datetime] = parse_time(metadata.get('ended_at', None))
self.metadata: Metadata = Metadata(metadata)
self.latest_invoice: Optional[SubscriptionInvoice] = (
SubscriptionInvoice(self, data=data['latest_invoice'], state=self._state) if 'latest_invoice' in data else None # type: ignore # ???
)
@property
def cancelled_at(self) -> Optional[datetime]:
"""Optional[:class:`datetime.datetime`]: When the subscription was canceled.
This is only available for subscriptions with a :attr:`status` of :attr:`SubscriptionStatus.canceled`.
This is an alias of :attr:`canceled_at`.
"""
return self.canceled_at
@property
def guild(self) -> Optional[Guild]:
""":class:`Guild`: The guild the subscription's entitlements apply to, if applicable."""
return self._state._get_guild(self.metadata.guild_id)
@property
def grace_period(self) -> int:
""":class:`int`: How many days past the renewal date the user has available to pay outstanding invoices.
.. note::
This is a static value and does not change based on the subscription's status.
For that, see :attr:`remaining`.
"""
return 7 if self.payment_source_id else 3
@property
def remaining(self) -> timedelta:
""":class:`datetime.timedelta`: The remaining time until the subscription ends."""
if self.status in (SubscriptionStatus.active, SubscriptionStatus.cancelled):
return self.current_period_end - utcnow()
elif self.status == SubscriptionStatus.past_due:
if self.payment_gateway == PaymentGateway.google and self.metadata.google_grace_period_expires_date:
return self.metadata.google_grace_period_expires_date - utcnow()
return (self.current_period_start + timedelta(days=self.grace_period)) - utcnow()
elif self.status == SubscriptionStatus.account_hold:
# Max hold time is 30 days
return (self.current_period_start + timedelta(days=30)) - utcnow()
return timedelta()
@property
def trial_remaining(self) -> timedelta:
""":class:`datetime.timedelta`: The remaining time until the trial applied to the subscription ends."""
if not self.trial_id:
return timedelta()
if not self.trial_ends_at:
# Infinite trial?
return self.remaining
return self.trial_ends_at - utcnow()
def is_active(self) -> bool:
""":class:`bool`: Indicates if the subscription is currently active and providing perks."""
return self.remaining > timedelta()
def is_trial(self) -> bool:
""":class:`bool`: Indicates if the subscription is a trial."""
return self.trial_id is not None
async def edit(
self,
items: List[SubscriptionItem] = MISSING,
payment_source: Snowflake = MISSING,
currency: str = MISSING,
*,
status: SubscriptionStatus = MISSING,
payment_source_token: Optional[str] = None,
) -> None:
"""|coro|
Edits the subscription.
All parameters are optional.
Parameters
----------
items: List[:class:`SubscriptionItem`]
The new subscription items to use.
payment_source: :class:`int`
The new payment source for payment.
currency: :class:`str`
The new currency to use for payment.
status: :class:`SubscriptionStatus`
The new status of the subscription.
payment_source_token: Optional[:class:`str`]
The token used to authorize with the payment source.
Raises
------
Forbidden
You do not have permissions to edit the subscription.
HTTPException
Editing the subscription failed.
"""
payload = {}
if items is not MISSING:
payload['items'] = [item.to_dict() for item in items] if items else []
if payment_source is not MISSING:
payload['payment_source_id'] = payment_source.id
payload['payment_source_token'] = payment_source_token
if currency is not MISSING:
payload['currency'] = currency
if status is not MISSING:
payload['status'] = int(status)
data = await self._state.http.edit_subscription(self.id, **payload)
self._update(data)
async def delete(self) -> None:
"""|coro|
Deletes the subscription.
There is an alias of this called :meth:`cancel`.
Raises
------
HTTPException
Deleting the subscription failed.
"""
await self._state.http.delete_subscription(self.id)
async def cancel(self) -> None:
"""|coro|
Deletes the subscription.
Alias of :meth:`delete`.
Raises
------
HTTPException
Deleting the subscription failed.
"""
await self.delete()
async def preview_invoice(
self,
*,
items: List[SubscriptionItem] = MISSING,
payment_source: Snowflake = MISSING,
currency: str = MISSING,
apply_entitlements: bool = MISSING,
renewal: bool = MISSING,
) -> SubscriptionInvoice:
"""|coro|
Preview an invoice for the subscription with the given parameters.
All parameters are optional and default to the current subscription values.
Parameters
----------
items: List[:class:`SubscriptionItem`]
The items the previewed invoice should have.
payment_source: :class:`.PaymentSource`
The payment source the previewed invoice should be paid with.
currency: :class:`str`
The currency the previewed invoice should be paid in.
apply_entitlements: :class:`bool`
Whether to apply entitlements (credits) to the previewed invoice.
renewal: :class:`bool`
Whether the previewed invoice should be a renewal.
Raises
------
HTTPException
Failed to preview the invoice.
Returns
-------
:class:`SubscriptionInvoice`
The previewed invoice.
"""
payload: Dict[str, Any] = {}
if items is not MISSING:
payload['items'] = [item.to_dict() for item in items] if items else []
if payment_source:
payload['payment_source_id'] = payment_source.id
if currency:
payload['currency'] = currency
if apply_entitlements is not MISSING:
payload['apply_entitlements'] = apply_entitlements
if renewal is not MISSING:
payload['renewal'] = renewal
if payload:
data = await self._state.http.preview_subscription_update(self.id, **payload)
else:
data = await self._state.http.get_subscription_preview(self.id)
return SubscriptionInvoice(self, data=data, state=self._state)
async def payment_source(self) -> Optional[PaymentSource]:
"""|coro|
Retrieves the payment source the subscription is paid with, if applicable.
Raises
------
NotFound
The payment source could not be found.
HTTPException
Retrieving the payment source failed.
Returns
-------
Optional[:class:`PaymentSource`]
The payment source the subscription is paid with, if applicable.
"""
if not self.payment_source_id:
return
data = await self._state.http.get_payment_source(self.payment_source_id)
return PaymentSource(data=data, state=self._state)
async def invoices(self):
"""|coro|
Retrieves all invoices for the subscription.
Raises
------
NotFound
The payment source or invoices could not be found.
HTTPException
Retrieving the invoices failed.
Returns
-------
List[:class:`SubscriptionInvoice`]
The invoices.
"""
state = self._state
data = await state.http.get_subscription_invoices(self.id)
return [SubscriptionInvoice(self, data=d, state=state) for d in data]
class SubscriptionTrial(Hashable):
"""Represents a subscription trial.
.. container:: operations
.. describe:: x == y
Checks if two trials are equal.
.. describe:: x != y
Checks if two trials are not equal.
.. describe:: hash(x)
Returns the trial's hash.
.. versionadded:: 2.0
Attributes
----------
id: :class:`int`
The ID of the trial.
interval: :class:`SubscriptionInterval`
The interval of the trial.
interval_count: :class:`int`
How many counts of the interval the trial provides.
"""
__slots__ = ('id', 'interval', 'interval_count', 'sku_id')
_INTERVAL_TABLE = {
SubscriptionInterval.day: 1,
SubscriptionInterval.month: 30,
SubscriptionInterval.year: 365,
}
def __init__(self, data: SubscriptionTrialPayload):
self.id: int = int(data['id'])
self.interval: SubscriptionInterval = try_enum(SubscriptionInterval, data['interval'])
self.interval_count: int = data['interval_count']
self.sku_id: int = int(data['sku_id'])
def __repr__(self) -> str:
return (
f'<SubscriptionTrial id={self.id} interval={self.interval} '
f'interval_count={self.interval_count} sku_id={self.sku_id}>'
)
@property
def duration(self) -> timedelta:
""":class:`datetime.timedelta`: How long the trial lasts."""
return timedelta(days=self.interval_count * self._INTERVAL_TABLE[self.interval])

344
discord/team.py

@ -24,31 +24,34 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
from datetime import datetime
from typing import TYPE_CHECKING, AsyncIterator, List, Optional, Union, overload
from . import utils
from .asset import Asset
from .enums import TeamMembershipState, try_enum
from .enums import ApplicationMembershipState, PayoutAccountStatus, PayoutReportType, PayoutStatus, try_enum
from .metadata import Metadata
from .mixins import Hashable
from .user import BaseUser
from typing import TYPE_CHECKING, Optional, overload, List, Union
from .object import Object
from .user import User, _UserTag
if TYPE_CHECKING:
from .abc import Snowflake
from .state import ConnectionState
from .types.team import (
Team as TeamPayload,
TeamMember as TeamMemberPayload,
)
from .types.user import User as UserPayload
from datetime import date
MISSING = utils.MISSING
from .abc import Snowflake, SnowflakeTime
from .appinfo import Application, Company
from .state import ConnectionState
from .types.team import Team as TeamPayload, TeamMember as TeamMemberPayload, TeamPayout as TeamPayoutPayload
from .types.user import PartialUser as PartialUserPayload
__all__ = (
'Team',
'TeamMember',
'TeamPayout',
)
MISSING = utils.MISSING
class Team(Hashable):
"""Represents an application team.
@ -71,6 +74,8 @@ class Team(Hashable):
Returns the team's name.
.. versionadded:: 2.0
Attributes
-------------
id: :class:`int`
@ -80,18 +85,36 @@ class Team(Hashable):
owner_id: :class:`int`
The team's owner ID.
members: List[:class:`TeamMember`]
A list of the members in the team.
A call to :meth:`fetch_members` may be required to populate this past the owner.
"""
The team's members.
.. note::
if TYPE_CHECKING:
owner_id: int
members: List[TeamMember]
In almost all cases, a call to :meth:`fetch_members`
is required to populate this list past (sometimes) the owner.
payout_account_status: Optional[:class:`PayoutAccountStatus`]
The team's payout account status, if any and available.
stripe_connect_account_id: Optional[:class:`str`]
The account ID representing the Stripe Connect account the
team's payout account is linked to, if any and available.
"""
__slots__ = ('_state', 'id', 'name', '_icon', 'owner_id', 'members')
__slots__ = (
'_state',
'id',
'name',
'_icon',
'owner_id',
'members',
'payout_account_status',
'stripe_connect_account_id',
)
def __init__(self, state: ConnectionState, data: TeamPayload):
self._state: ConnectionState = state
self.members: List[TeamMember] = []
self.payout_account_status: Optional[PayoutAccountStatus] = None
self.stripe_connect_account_id: Optional[str] = None
self._update(data)
def __repr__(self) -> str:
@ -101,20 +124,32 @@ class Team(Hashable):
return self.name
def _update(self, data: TeamPayload):
state = self._state
self.id: int = int(data['id'])
self.name: str = data['name']
self._icon: Optional[str] = data['icon']
self.owner_id = owner_id = int(data['owner_user_id'])
self.members = members = [TeamMember(self, self._state, member) for member in data.get('members', [])]
if owner_id not in members and owner_id == self._state.self_id: # Discord moment
user: UserPayload = self._state.user._to_minimal_user_json() # type: ignore
member: TeamMemberPayload = {
'user': user,
'team_id': self.id,
'membership_state': 2,
'permissions': ['*'],
}
members.append(TeamMember(self, self._state, member))
if 'members' in data:
self.members = [TeamMember(self, state=state, data=member) for member in data.get('members', [])]
if not self.owner:
owner = self._state.get_user(owner_id)
if owner:
user: PartialUserPayload = owner._to_minimal_user_json()
member: TeamMemberPayload = {
'user': user,
'team_id': self.id,
'membership_state': 2,
'permissions': ['*'],
}
self.members.append(TeamMember(self, self._state, member))
if 'payout_account_status' in data:
self.payout_account_status = try_enum(PayoutAccountStatus, data.get('payout_account_status'))
if 'stripe_connect_account_id' in data:
self.stripe_connect_account_id = data.get('stripe_connect_account_id')
@property
def icon(self) -> Optional[Asset]:
@ -123,9 +158,22 @@ class Team(Hashable):
return None
return Asset._from_icon(self._state, self.id, self._icon, path='team')
@property
def default_icon(self) -> Asset:
""":class:`Asset`: Returns the default icon for the team. This is calculated by the team's ID."""
return Asset._from_default_avatar(self._state, int(self.id) % 5)
@property
def display_icon(self) -> Asset:
""":class:`Asset`: Returns the team's display icon.
For regular teams this is just their default icon or uploaded icon.
"""
return self.icon or self.default_icon
@property
def owner(self) -> Optional[TeamMember]:
"""Optional[:class:`TeamMember`]: The team's owner."""
"""Optional[:class:`TeamMember`]: The team's owner, if available."""
return utils.get(self.members, id=self.owner_id)
async def edit(
@ -139,13 +187,15 @@ class Team(Hashable):
Edits the team.
All parameters are optional.
Parameters
-----------
name: :class:`str`
The name of the team.
icon: Optional[:class:`bytes`]
The icon of the team.
owner: :class:`~abc.Snowflake`
owner: :class:`User`
The team's owner.
Raises
@ -169,10 +219,33 @@ class Team(Hashable):
data = await self._state.http.edit_team(self.id, payload)
self._update(data)
async def applications(self) -> List[Application]:
"""|coro|
Retrieves the team's applications.
Returns
--------
List[:class:`TeamMember`]
The team's applications.
Raises
-------
Forbidden
You do not have permissions to fetch the team's applications.
HTTPException
Retrieving the team applications failed.
"""
from .appinfo import Application
state = self._state
data = await state.http.get_team_applications(self.id)
return [Application(state=state, data=app, team=self) for app in data]
async def fetch_members(self) -> List[TeamMember]:
"""|coro|
Retrieves the team's members.
Retrieves and caches the team's members.
Returns
--------
@ -192,7 +265,7 @@ class Team(Hashable):
return members
@overload
async def invite_member(self, user: BaseUser, /) -> TeamMember:
async def invite_member(self, user: _UserTag, /) -> TeamMember:
...
@overload
@ -203,7 +276,7 @@ class Team(Hashable):
async def invite_member(self, username: str, discriminator: str, /) -> TeamMember:
...
async def invite_member(self, *args: Union[BaseUser, str]) -> TeamMember:
async def invite_member(self, *args: Union[_UserTag, str]) -> TeamMember:
"""|coro|
Invites a member to the team.
@ -241,14 +314,14 @@ class Team(Hashable):
Returns
-------
:class:`.TeamMember`
:class:`TeamMember`
The new member.
"""
username: str
discrim: str
if len(args) == 1:
user = args[0]
if isinstance(user, BaseUser):
if isinstance(user, _UserTag):
user = str(user)
username, discrim = user.split('#')
elif len(args) == 2:
@ -262,6 +335,112 @@ class Team(Hashable):
self.members.append(member)
return member
async def create_company(self, name: str, /) -> Company:
"""|coro|
Creates a company for the team.
Parameters
-----------
name: :class:`str`
The name of the company.
Raises
-------
Forbidden
You do not have permissions to create a company.
HTTPException
Creating the company failed.
Returns
-------
:class:`.Company`
The created company.
"""
from .appinfo import Company
state = self._state
data = await state.http.create_team_company(self.id, name)
return Company(data=data)
async def payouts(
self,
*,
limit: Optional[int] = 96,
before: Optional[SnowflakeTime] = None,
) -> AsyncIterator[TeamPayout]:
"""Returns an :term:`asynchronous iterator` that enables receiving your team payouts.
.. versionadded:: 2.0
Examples
---------
Usage ::
total = 0
async for payout in team.payouts():
if payout.period_end:
total += payout.amount
Flattening into a list: ::
payments = [payout async for payout in team.payouts(limit=123)]
# payments is now a list of TeamPayout...
All parameters are optional.
Parameters
-----------
limit: Optional[:class:`int`]
The number of payouts to retrieve.
If ``None``, retrieves every payout you have. Note, however,
that this would make it a slow operation.
before: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]]
Retrieve payments before this date or payout.
If a datetime is provided, it is recommended to use a UTC aware datetime.
If the datetime is naive, it is assumed to be local time.
Raises
------
HTTPException
The request to get team payouts failed.
Yields
-------
:class:`~discord.TeamPayout`
The payout received.
"""
async def strategy(retrieve, before, limit):
before_id = before.id if before else None
data = await self._state.http.get_team_payouts(self.id, limit=retrieve, before=before_id)
if data:
if limit is not None:
limit -= len(data)
before = Object(id=int(data[-1]['id']))
return data, before, limit
if isinstance(before, datetime):
before = Object(id=utils.time_snowflake(before, high=False))
while True:
retrieve = min(96 if limit is None else limit, 100)
if retrieve < 1:
return
data, before, limit = await strategy(retrieve, before, limit)
# Terminate loop on next iteration; there's no data left after this
if len(data) < 96:
limit = 0
for payout in data:
yield TeamPayout(data=payout, team=self)
async def delete(self) -> None:
"""|coro|
@ -277,7 +456,7 @@ class Team(Hashable):
await self._state.http.delete_team(self.id)
class TeamMember(BaseUser):
class TeamMember(User):
"""Represents a team member in a team.
.. container:: operations
@ -304,16 +483,18 @@ class TeamMember(BaseUser):
-------------
team: :class:`Team`
The team that the member is from.
membership_state: :class:`TeamMembershipState`
membership_state: :class:`ApplicationMembershipState`
The membership state of the member (i.e. invited or accepted)
permissions: List[:class:`str`]
The permissions of the team member. This is always "*".
"""
__slots__ = ('team', 'membership_state', 'permissions')
def __init__(self, team: Team, state: ConnectionState, data: TeamMemberPayload):
self.team: Team = team
self.membership_state: TeamMembershipState = try_enum(TeamMembershipState, data['membership_state'])
self.permissions: List[str] = data['permissions']
self.membership_state: ApplicationMembershipState = try_enum(ApplicationMembershipState, data['membership_state'])
self.permissions: List[str] = data.get('permissions', ['*'])
super().__init__(state=state, data=data['user'])
def __repr__(self) -> str:
@ -335,3 +516,84 @@ class TeamMember(BaseUser):
Removing the member failed.
"""
await self._state.http.remove_team_member(self.team.id, self.id)
class TeamPayout(Hashable):
"""Represents a team payout.
.. container:: operations
.. describe:: x == y
Checks if two team payouts are equal.
.. describe:: x != y
Checks if two team payouts are not equal.
.. describe:: hash(x)
Return the team payout's hash.
.. versionadded:: 2.0
Attributes
-----------
id: :class:`int`
The ID of the payout.
user_id: :class:`int`
The ID of the user who is to be receiving the payout.
status: :class:`PayoutStatus`
The status of the payout.
amount: :class:`int`
The amount of the payout.
period_start: :class:`datetime.date`
The start of the payout period.
period_end: Optional[:class:`datetime.date`]
The end of the payout period, if ended.
payout_date: Optional[:class:`datetime.date`]
The date the payout was made, if made.
tipalti_submission_response: Optional[:class:`Metadata`]
The latest response from Tipalti, if exists.
"""
def __init__(self, *, data: TeamPayoutPayload, team: Team):
self.team: Team = team
self.id: int = int(data['id'])
self.user_id: int = int(data['user_id'])
self.status: PayoutStatus = try_enum(PayoutStatus, data['status'])
self.amount: int = data['amount']
self.period_start: date = utils.parse_date(data['period_start'])
self.period_end: Optional[date] = utils.parse_date(data.get('period_end'))
self.payout_date: Optional[date] = utils.parse_date(data.get('payout_date'))
self.tipalti_submission_response: Optional[Metadata] = (
Metadata(data['latest_tipalti_submission_response']) if 'latest_tipalti_submission_response' in data else None
)
def __repr__(self) -> str:
return f'<{self.__class__.__name__} id={self.id} status={self.status!r}>'
async def report(self, type: PayoutReportType) -> bytes:
"""|coro|
Returns the report for the payout in CSV format.
Parameters
-----------
type: :class:`PayoutReportType`
The type of report to get the URL for.
Raises
-------
Forbidden
You do not have permissions to get the report URL.
HTTPException
Getting the report URL failed.
Returns
-------
:class:`bytes`
The report content.
"""
return await self.team._state.http.get_team_payout_report(self.team.id, self.id, str(type))

16
discord/tracking.py

@ -88,6 +88,7 @@ class ContextProperties: # Thank you Discord-S.C.U.M
# Locations
'Friends': 'eyJsb2NhdGlvbiI6IkZyaWVuZHMifQ==',
'ContextMenu': 'eyJsb2NhdGlvbiI6IkNvbnRleHRNZW51In0=',
'Context Menu': 'eyJsb2NhdGlvbiI6IkNvbnRleHQgTWVudSJ9',
'User Profile': 'eyJsb2NhdGlvbiI6IlVzZXIgUHJvZmlsZSJ9',
'Add Friend': 'eyJsb2NhdGlvbiI6IkFkZCBGcmllbmQifQ==',
'Guild Header': 'eyJsb2NhdGlvbiI6Ikd1aWxkIEhlYWRlciJ9',
@ -100,6 +101,7 @@ class ContextProperties: # Thank you Discord-S.C.U.M
'New Group DM': 'eyJsb2NhdGlvbiI6Ik5ldyBHcm91cCBETSJ9',
'Add Friends to DM': 'eyJsb2NhdGlvbiI6IkFkZCBGcmllbmRzIHRvIERNIn0=',
'Group DM Invite Create': 'eyJsb2NhdGlvbiI6Ikdyb3VwIERNIEludml0ZSBDcmVhdGUifQ==',
'Stage Channel': 'eyJsb2NhdGlvbiI6IlN0YWdlIENoYW5uZWwifQ==',
# Sources
'Chat Input Blocker - Lurker Mode': 'eyJzb3VyY2UiOiJDaGF0IElucHV0IEJsb2NrZXIgLSBMdXJrZXIgTW9kZSJ9',
'Notice - Lurker Mode': 'eyJzb3VyY2UiOiJOb3RpY2UgLSBMdXJrZXIgTW9kZSJ9',
@ -120,10 +122,15 @@ class ContextProperties: # Thank you Discord-S.C.U.M
return cls(data)
@classmethod
def _from_context_menu(cls) -> Self:
def _from_contextmenu(cls) -> Self:
data = {'location': 'ContextMenu'}
return cls(data)
@classmethod
def _from_context_menu(cls) -> Self:
data = {'location': 'Context Menu'}
return cls(data)
@classmethod
def _from_user_profile(cls) -> Self:
data = {'location': 'User Profile'}
@ -135,7 +142,7 @@ class ContextProperties: # Thank you Discord-S.C.U.M
return cls(data)
@classmethod
def _from_guild_header_menu(cls) -> Self:
def _from_guild_header(cls) -> Self:
data = {'location': 'Guild Header'}
return cls(data)
@ -184,6 +191,11 @@ class ContextProperties: # Thank you Discord-S.C.U.M
data = {'location': 'Verify Email'}
return cls(data)
@classmethod
def _from_stage_channel(cls) -> Self:
data = {'location': 'Stage Channel'}
return cls(data)
@classmethod
def _from_accept_invite_page(
cls,

203
discord/types/appinfo.py

@ -24,49 +24,204 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
from typing import TypedDict, List, Optional
from typing import Dict, List, Literal, Optional, TypedDict, Union
from typing_extensions import NotRequired
from .user import User
from .team import Team
from .guild import PartialGuild
from .snowflake import Snowflake
from .team import Team
from .user import PartialUser
class BaseAppInfo(TypedDict):
class BaseApplication(TypedDict):
id: Snowflake
name: str
verify_key: str
icon: Optional[str]
summary: str
description: str
cover_image: Optional[str]
flags: NotRequired[int]
rpc_origins: List[str]
icon: Optional[str]
cover_image: NotRequired[Optional[str]]
type: Optional[int]
primary_sku_id: NotRequired[Snowflake]
summary: NotRequired[Literal['']]
class IntegrationApplication(BaseApplication):
bot: NotRequired[PartialUser]
role_connections_verification_url: NotRequired[Optional[str]]
class AppInfo(BaseAppInfo):
owner: User
class PartialApplication(BaseApplication):
owner: NotRequired[PartialUser] # Not actually ever present in partial app
team: NotRequired[Team]
verify_key: str
description: str
cover_image: NotRequired[Optional[str]]
flags: NotRequired[int]
rpc_origins: NotRequired[List[str]]
hook: NotRequired[bool]
overlay: NotRequired[bool]
overlay_compatibility_hook: NotRequired[bool]
terms_of_service_url: NotRequired[str]
privacy_policy_url: NotRequired[str]
max_participants: NotRequired[Optional[int]]
bot_public: NotRequired[bool]
bot_require_code_grant: NotRequired[bool]
integration_public: NotRequired[bool]
integration_require_code_grant: NotRequired[bool]
team: NotRequired[Team]
guild_id: NotRequired[Snowflake]
primary_sku_id: NotRequired[Snowflake]
slug: NotRequired[str]
terms_of_service_url: NotRequired[str]
privacy_policy_url: NotRequired[str]
hook: NotRequired[bool]
max_participants: NotRequired[int]
interactions_endpoint_url: NotRequired[str]
developers: NotRequired[List[Company]]
publishers: NotRequired[List[Company]]
aliases: NotRequired[List[str]]
eula_id: NotRequired[Snowflake]
embedded_activity_config: NotRequired[EmbeddedActivityConfig]
guild: NotRequired[PartialGuild]
class ApplicationDiscoverability(TypedDict):
discoverability_state: int
discovery_eligibility_flags: int
class Application(PartialApplication, IntegrationApplication, ApplicationDiscoverability):
redirect_uris: List[str]
interactions_endpoint_url: Optional[str]
verification_state: int
store_application_state: int
rpc_application_state: int
interactions_endpoint_url: str
creator_monetization_state: int
role_connections_verification_url: NotRequired[Optional[str]]
class WhitelistedUser(TypedDict):
user: PartialUser
state: Literal[1, 2]
class Asset(TypedDict):
id: Snowflake
name: str
type: int
class StoreAsset(TypedDict):
id: Snowflake
size: int
width: int
height: int
mime_type: str
class Company(TypedDict):
id: Snowflake
name: str
class EULA(TypedDict):
id: Snowflake
name: str
content: str
class BaseAchievement(TypedDict):
id: Snowflake
name: Union[str, Dict[str, Union[str, Dict[str, str]]]]
name_localizations: NotRequired[Dict[str, str]]
description: Union[str, Dict[str, Union[str, Dict[str, str]]]]
description_localizations: NotRequired[Dict[str, str]]
icon_hash: str
secure: bool
secret: bool
class Achievement(BaseAchievement):
application_id: Snowflake
class Ticket(TypedDict):
ticket: str
class Branch(TypedDict):
id: Snowflake
live_build_id: NotRequired[Optional[Snowflake]]
created_at: NotRequired[str]
name: NotRequired[str]
class BranchSize(TypedDict):
size_kb: str # Stringified float
class DownloadSignature(TypedDict):
endpoint: str
expires: int
signature: str
class Build(TypedDict):
application_id: NotRequired[Snowflake]
created_at: NotRequired[str]
id: Snowflake
manifests: List[Manifest]
status: Literal['CORRUPTED', 'INVALID', 'READY', 'VALIDATING', 'UPLOADED', 'UPLOADING', 'CREATED']
source_build_id: NotRequired[Optional[Snowflake]]
version: NotRequired[Optional[str]]
class CreatedBuild(TypedDict):
build: Build
manifest_uploads: List[Manifest]
class BuildFile(TypedDict):
id: Snowflake
md5_hash: NotRequired[str]
class CreatedBuildFile(TypedDict):
id: str
url: str
class ManifestLabel(TypedDict):
application_id: Snowflake
id: Snowflake
name: NotRequired[str]
class Manifest(TypedDict):
id: Snowflake
label: ManifestLabel
redistributable_label_ids: NotRequired[List[Snowflake]]
url: Optional[str]
class ActivityStatistics(TypedDict):
application_id: NotRequired[Snowflake]
user_id: NotRequired[Snowflake]
total_duration: int
total_discord_sku_duration: NotRequired[int]
last_played_at: str
class GlobalActivityStatistics(TypedDict):
application_id: Snowflake
user_id: Snowflake
duration: int
updated_at: str
class EmbeddedActivityConfig(TypedDict):
supported_platforms: List[Literal['web', 'android', 'ios']]
default_orientation_lock_state: Literal[1, 2, 3]
activity_preview_video_asset_id: NotRequired[Optional[Snowflake]]
class ActiveDeveloperWebhook(TypedDict):
channel_id: Snowflake
webhook_id: Snowflake
class PartialAppInfo(BaseAppInfo, total=False):
hook: bool
terms_of_service_url: str
privacy_policy_url: str
max_participants: int
class ActiveDeveloperResponse(TypedDict):
follower: ActiveDeveloperWebhook

78
discord/types/billing.py

@ -0,0 +1,78 @@
"""
The MIT License (MIT)
Copyright (c) 2021-present Dolfies
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import Literal, TypedDict
from typing_extensions import NotRequired
class BillingAddress(TypedDict):
line_1: str
line_2: NotRequired[str]
name: str
postal_code: NotRequired[str]
city: str
state: NotRequired[str]
country: str
email: NotRequired[str]
class BillingAddressToken(TypedDict):
token: str
class PartialPaymentSource(TypedDict):
id: str
brand: NotRequired[str]
country: NotRequired[str]
last_4: NotRequired[str]
type: Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
payment_gateway: Literal[1, 2, 3, 4, 5, 6]
invalid: bool
flags: int
expires_month: NotRequired[int]
expires_year: NotRequired[int]
email: NotRequired[str]
bank: NotRequired[str]
username: NotRequired[str]
screen_status: int # TODO: Figure this out
class PaymentSource(PartialPaymentSource):
billing_address: BillingAddress
default: bool
class PremiumUsageValue(TypedDict):
value: int
class PremiumUsage(TypedDict):
nitro_sticker_sends: PremiumUsageValue
total_animated_emojis: PremiumUsageValue
total_global_emojis: PremiumUsageValue
total_large_uploads: PremiumUsageValue
total_hd_streams: PremiumUsageValue
hd_hours_streamed: PremiumUsageValue

6
discord/types/channel.py

@ -57,8 +57,14 @@ class _BaseGuildChannel(_BaseChannel):
parent_id: Optional[Snowflake]
class PartialRecipient(TypedDict):
username: str
class PartialChannel(_BaseChannel):
type: ChannelType
icon: NotRequired[Optional[str]]
recipients: NotRequired[List[PartialRecipient]]
class _BaseTextChannel(_BaseGuildChannel, total=False):

95
discord/types/entitlements.py

@ -0,0 +1,95 @@
"""
The MIT License (MIT)
Copyright (c) 2021-present Dolfies
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import List, Literal, Optional, TypedDict
from typing_extensions import NotRequired
from .payments import PartialPayment
from .promotions import Promotion
from .snowflake import Snowflake
from .store import SKU, StoreListing
from .subscriptions import PartialSubscriptionPlan, SubscriptionPlan, SubscriptionTrial
from .user import PartialUser
class Entitlement(TypedDict):
id: Snowflake
type: Literal[1, 2, 3, 4, 5, 6, 7]
user_id: Snowflake
sku_id: Snowflake
application_id: Snowflake
promotion_id: Optional[Snowflake]
parent_id: NotRequired[Snowflake]
guild_id: NotRequired[Snowflake]
branches: NotRequired[List[Snowflake]]
gifter_user_id: NotRequired[Snowflake]
gift_style: NotRequired[Literal[1, 2, 3]]
gift_batch_id: NotRequired[Snowflake]
gift_code_flags: NotRequired[int]
deleted: bool
consumed: NotRequired[bool]
starts_at: NotRequired[str]
ends_at: NotRequired[str]
subscription_id: NotRequired[Snowflake]
subscription_plan: NotRequired[PartialSubscriptionPlan]
sku: NotRequired[SKU]
payment: NotRequired[PartialPayment]
class GatewayGift(TypedDict):
code: str
uses: int
sku_id: Snowflake
channel_id: NotRequired[Snowflake]
guild_id: NotRequired[Snowflake]
class Gift(GatewayGift):
expires_at: Optional[str]
application_id: Snowflake
batch_id: NotRequired[Snowflake]
entitlement_branches: NotRequired[List[Snowflake]]
gift_style: NotRequired[Optional[Literal[1, 2, 3]]]
flags: int
max_uses: int
uses: int
redeemed: bool
revoked: NotRequired[bool]
store_listing: NotRequired[StoreListing]
promotion: NotRequired[Promotion]
subscription_trial: NotRequired[SubscriptionTrial]
subscription_plan: NotRequired[SubscriptionPlan]
user: NotRequired[PartialUser]
class GiftBatch(TypedDict):
id: Snowflake
sku_id: Snowflake
amount: int
description: NotRequired[str]
entitlement_branches: NotRequired[List[Snowflake]]
entitlement_starts_at: NotRequired[str]
entitlement_ends_at: NotRequired[str]

96
discord/types/gateway.py

@ -37,12 +37,16 @@ from .member import MemberWithUser
from .snowflake import Snowflake
from .message import Message
from .sticker import GuildSticker
from .appinfo import PartialAppInfo
from .appinfo import BaseAchievement, PartialApplication
from .guild import Guild, UnavailableGuild, SupplementalGuild
from .user import Connection, User
from .user import Connection, User, PartialUser
from .threads import Thread, ThreadMember
from .scheduled_event import GuildScheduledEvent
from .channel import DMChannel, GroupDMChannel
from .subscriptions import PremiumGuildSubscriptionSlot
from .payments import Payment
from .entitlements import Entitlement, GatewayGift
from .library import LibraryApplication
PresenceUpdateEvent = PartialPresenceUpdate
@ -57,8 +61,14 @@ class ShardInfo(TypedDict):
shard_count: int
class ReadyEvent(TypedDict):
class ResumedEvent(TypedDict):
_trace: List[str]
class ReadyEvent(ResumedEvent):
api_code_version: int
analytics_token: str
auth_session_id_hash: str
auth_token: NotRequired[str]
connected_accounts: List[Connection]
country_code: str
@ -66,17 +76,19 @@ class ReadyEvent(TypedDict):
geo_ordered_rtc_regions: List[str]
guilds: List[Guild]
merged_members: List[List[MemberWithUser]]
pending_payments: NotRequired[List[Payment]]
private_channels: List[Union[DMChannel, GroupDMChannel]]
relationships: List[dict]
required_action: NotRequired[str]
sessions: List[dict]
session_id: str
session_type: str
shard: NotRequired[ShardInfo]
user: User
user_guild_settings: dict
user_settings: dict
user_settings: NotRequired[dict]
user_settings_proto: str
users: List[User]
users: List[PartialUser]
v: int
@ -91,7 +103,7 @@ class ReadySupplementalEvent(TypedDict):
merged_presences: MergedPresences
ResumedEvent = Literal[None]
NoEvent = Literal[None]
MessageCreateEvent = Message
@ -157,10 +169,10 @@ class InviteCreateEvent(TypedDict):
temporary: bool
uses: Literal[0]
guild_id: NotRequired[Snowflake]
inviter: NotRequired[User]
inviter: NotRequired[PartialUser]
target_type: NotRequired[InviteTargetType]
target_user: NotRequired[User]
target_application: NotRequired[PartialAppInfo]
target_user: NotRequired[PartialUser]
target_application: NotRequired[PartialApplication]
class InviteDeleteEvent(TypedDict):
@ -203,6 +215,7 @@ class ThreadListSyncEvent(TypedDict):
threads: List[Thread]
members: List[ThreadMember]
channel_ids: NotRequired[List[Snowflake]]
most_recent_messages: List[Message]
class ThreadMemberUpdate(ThreadMember):
@ -221,15 +234,19 @@ class GuildMemberAddEvent(MemberWithUser):
guild_id: Snowflake
class SnowflakeUser(TypedDict):
id: Snowflake
class GuildMemberRemoveEvent(TypedDict):
guild_id: Snowflake
user: User
user: Union[PartialUser, SnowflakeUser]
class GuildMemberUpdateEvent(TypedDict):
guild_id: Snowflake
roles: List[Snowflake]
user: User
user: PartialUser
avatar: Optional[str]
joined_at: Optional[str]
nick: NotRequired[str]
@ -256,7 +273,7 @@ GuildDeleteEvent = UnavailableGuild
class _GuildBanEvent(TypedDict):
guild_id: Snowflake
user: User
user: PartialUser
GuildBanAddEvent = GuildBanRemoveEvent = _GuildBanEvent
@ -341,3 +358,58 @@ class TypingStartEvent(TypedDict):
timestamp: int
guild_id: NotRequired[Snowflake]
member: NotRequired[MemberWithUser]
ConnectionEvent = Connection
class PartialConnectionEvent(TypedDict):
user_id: Snowflake
class ConnectionsLinkCallbackEvent(TypedDict):
provider: str
callback_code: str
callback_state: str
class OAuth2TokenRevokeEvent(TypedDict):
access_token: str
class AuthSessionChangeEvent(TypedDict):
auth_session_id_hash: str
class PaymentClientAddEvent(TypedDict):
purchase_token_hash: str
expires_at: str
class AchievementUpdatePayload(TypedDict):
application_id: Snowflake
achievement: BaseAchievement
percent_complete: int
PremiumGuildSubscriptionSlotEvent = PremiumGuildSubscriptionSlot
class RequiredActionEvent(TypedDict):
required_action: str
class BillingPopupBridgeCallbackEvent(TypedDict):
payment_source_type: int
state: str
path: str
query: str
PaymentUpdateEvent = Payment
GiftCreateEvent = GiftUpdateEvent = GatewayGift
EntitlementEvent = Entitlement
LibraryApplicationUpdateEvent = LibraryApplication

6
discord/types/guild.py

@ -57,7 +57,7 @@ NSFWLevel = Literal[0, 1, 2, 3]
PremiumTier = Literal[0, 1, 2, 3]
class _BaseGuildPreview(UnavailableGuild):
class PartialGuild(UnavailableGuild):
name: str
icon: Optional[str]
splash: Optional[str]
@ -73,11 +73,11 @@ class _GuildPreviewUnique(TypedDict):
approximate_presence_count: int
class GuildPreview(_BaseGuildPreview, _GuildPreviewUnique):
class GuildPreview(PartialGuild, _GuildPreviewUnique):
...
class Guild(_BaseGuildPreview):
class Guild(PartialGuild):
owner_id: Snowflake
region: str
afk_channel_id: Optional[Snowflake]

24
discord/types/integration.py

@ -24,23 +24,15 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
from typing import Literal, Optional, TypedDict, Union
from typing import List, Literal, Optional, TypedDict, Union
from typing_extensions import NotRequired
from .appinfo import IntegrationApplication
from .guild import Guild
from .snowflake import Snowflake
from .user import User
class IntegrationApplication(TypedDict):
id: Snowflake
name: str
icon: Optional[str]
description: str
summary: str
bot: NotRequired[User]
class IntegrationAccount(TypedDict):
id: str
name: str
@ -54,6 +46,7 @@ class PartialIntegration(TypedDict):
name: str
type: IntegrationType
account: IntegrationAccount
application_id: NotRequired[Snowflake]
IntegrationType = Literal['twitch', 'youtube', 'discord']
@ -61,11 +54,7 @@ IntegrationType = Literal['twitch', 'youtube', 'discord']
class BaseIntegration(PartialIntegration):
enabled: bool
syncing: bool
synced_at: str
user: User
expire_behavior: IntegrationExpireBehavior
expire_grace_period: int
user: NotRequired[User]
class StreamIntegration(BaseIntegration):
@ -73,10 +62,15 @@ class StreamIntegration(BaseIntegration):
enable_emoticons: bool
subscriber_count: int
revoked: bool
expire_behavior: IntegrationExpireBehavior
expire_grace_period: int
syncing: bool
synced_at: str
class BotIntegration(BaseIntegration):
application: IntegrationApplication
scopes: List[str]
class ConnectionIntegration(BaseIntegration):

6
discord/types/invite.py

@ -32,7 +32,7 @@ from .snowflake import Snowflake
from .guild import InviteGuild, _GuildPreviewUnique
from .channel import PartialChannel
from .user import PartialUser
from .appinfo import PartialAppInfo
from .appinfo import PartialApplication
InviteTargetType = Literal[1, 2]
@ -61,7 +61,7 @@ class Invite(IncompleteInvite, total=False):
inviter: PartialUser
target_user: PartialUser
target_type: InviteTargetType
target_application: PartialAppInfo
target_application: PartialApplication
guild_scheduled_event: GuildScheduledEvent
@ -81,7 +81,7 @@ class GatewayInviteCreate(TypedDict):
inviter: NotRequired[PartialUser]
target_type: NotRequired[InviteTargetType]
target_user: NotRequired[PartialUser]
target_application: NotRequired[PartialAppInfo]
target_application: NotRequired[PartialApplication]
class GatewayInviteDelete(TypedDict):

44
discord/types/library.py

@ -0,0 +1,44 @@
"""
The MIT License (MIT)
Copyright (c) 2021-present Dolfies
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import List, TypedDict
from typing_extensions import NotRequired
from .appinfo import Branch, PartialApplication
from .entitlements import Entitlement
from .snowflake import Snowflake
from .store import PartialSKU
class LibraryApplication(TypedDict):
created_at: str
application: PartialApplication
sku_id: Snowflake
sku: PartialSKU
entitlements: List[Entitlement]
flags: int
branch_id: Snowflake
branch: NotRequired[Branch]

61
discord/types/payments.py

@ -0,0 +1,61 @@
"""
The MIT License (MIT)
Copyright (c) 2021-present Dolfies
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import List, Literal, TypedDict
from typing_extensions import NotRequired
from .billing import PartialPaymentSource
from .snowflake import Snowflake
from .store import SKU
from .subscriptions import PartialSubscription
class PartialPayment(TypedDict):
# TODO: There might be more, but I don't have an example payload
id: Snowflake
amount: int
tax: int
tax_inclusive: bool
currency: str
class Payment(PartialPayment):
amount_refunded: int
description: str
status: Literal[0, 1, 2, 3, 4, 5]
created_at: str
sku_id: NotRequired[Snowflake]
sku_price: NotRequired[int]
sku_subscription_plan_id: NotRequired[Snowflake]
payment_gateway: NotRequired[Literal[1, 2, 3, 4, 5, 6]]
payment_gateway_payment_id: NotRequired[str]
downloadable_invoice: NotRequired[str]
downloadable_refund_invoices: NotRequired[List[str]]
refund_disqualification_reasons: NotRequired[List[str]]
flags: int
sku: NotRequired[SKU]
payment_source: NotRequired[PartialPaymentSource]
subscription: NotRequired[PartialSubscription]

79
discord/types/promotions.py

@ -0,0 +1,79 @@
"""
The MIT License (MIT)
Copyright (c) 2021-present Dolfies
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import List, Optional, TypedDict
from typing_extensions import NotRequired
from .snowflake import Snowflake
from .subscriptions import SubscriptionTrial
class Promotion(TypedDict):
id: Snowflake
trial_id: NotRequired[Snowflake]
start_date: str
end_date: str
flags: int
outbound_title: str
outbound_redemption_modal_body: str
outbound_redemption_page_link: NotRequired[str]
outbound_redemption_url_format: NotRequired[str]
outbound_restricted_countries: NotRequired[List[str]]
outbound_terms_and_conditions: str
inbound_title: NotRequired[str]
inbound_body_text: NotRequired[str]
inbound_help_center_link: NotRequired[str]
inbound_restricted_countries: NotRequired[List[str]]
class ClaimedPromotion(TypedDict):
promotion: Promotion
code: str
claimed_at: str
class TrialOffer(TypedDict):
id: Snowflake
expires_at: str
trial_id: Snowflake
subscription_trial: SubscriptionTrial
class PromotionalPrice(TypedDict):
amount: int
currency: str
class PricingPromotion(TypedDict):
plan_id: Snowflake
country_code: str
payment_source_types: List[str]
price: PromotionalPrice
class WrappedPricingPromotion(TypedDict):
country_code: str
localized_pricing_promo: Optional[PricingPromotion]

152
discord/types/store.py

@ -0,0 +1,152 @@
"""
The MIT License (MIT)
Copyright (c) 2021-present Dolfies
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import Dict, List, Literal, Optional, TypedDict, Union
from typing_extensions import NotRequired
from .appinfo import PartialApplication, StoreAsset
from .entitlements import Entitlement
from .guild import PartialGuild
from .library import LibraryApplication
from .snowflake import Snowflake
from .user import PartialUser
LOCALIZED_STR = Union[str, Dict[str, str]]
class StoreNote(TypedDict):
content: str
user: Optional[PartialUser]
class SystemRequirement(TypedDict, total=False):
ram: int
disk: int
operating_system_version: LOCALIZED_STR
cpu: LOCALIZED_STR
gpu: LOCALIZED_STR
sound_card: LOCALIZED_STR
directx: LOCALIZED_STR
network: LOCALIZED_STR
notes: LOCALIZED_STR
class SystemRequirements(TypedDict, total=False):
minimum: SystemRequirement
recommended: SystemRequirement
class CarouselItem(TypedDict, total=False):
asset_id: Snowflake
youtube_video_id: str
class StoreListing(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]
header_background: NotRequired[StoreAsset]
hero_background: NotRequired[StoreAsset]
hero_video: NotRequired[StoreAsset]
box_art: NotRequired[StoreAsset]
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]]
class SKUPrice(TypedDict):
currency: str
amount: int
sale_amount: NotRequired[Optional[int]]
sale_percentage: NotRequired[int]
premium: NotRequired[bool]
class ContentRating(TypedDict):
rating: int
descriptors: List[int]
class PartialSKU(TypedDict):
id: Snowflake
type: Literal[1, 2, 3, 4, 5, 6]
premium: bool
preorder_release_date: Optional[str]
preorder_released_at: Optional[str]
class SKU(PartialSKU):
id: Snowflake
type: Literal[1, 2, 3, 4, 5, 6]
name: LOCALIZED_STR
summary: NotRequired[LOCALIZED_STR]
legal_notice: NotRequired[LOCALIZED_STR]
slug: str
dependent_sku_id: Optional[Snowflake]
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]]
preorder_released_at: NotRequired[Optional[str]]
external_purchase_url: NotRequired[str]
premium: NotRequired[bool]
restricted: NotRequired[bool]
exclusive: NotRequired[bool]
show_age_gate: bool
bundled_skus: NotRequired[List[SKU]]
manifest_labels: Optional[List[Snowflake]]
class SKUPurchase(TypedDict):
entitlements: List[Entitlement]
library_applications: NotRequired[List[LibraryApplication]]
gift_code: NotRequired[str]

161
discord/types/subscriptions.py

@ -0,0 +1,161 @@
"""
The MIT License (MIT)
Copyright (c) 2021-present Dolfies
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import Any, Dict, List, Literal, Optional, TypedDict
from typing_extensions import NotRequired
from .snowflake import Snowflake
from .user import PartialUser
class PremiumGuildSubscription(TypedDict):
id: Snowflake
guild_id: Snowflake
user_id: Snowflake
user: NotRequired[PartialUser]
ended: bool
ends_at: NotRequired[str]
class PremiumGuildSubscriptionSlot(TypedDict):
id: Snowflake
subscription_id: Snowflake
canceled: bool
cooldown_ends_at: Optional[str]
premium_guild_subscription: Optional[PremiumGuildSubscription]
class PremiumGuildSubscriptionCooldown(TypedDict):
ends_at: str
limit: int
remaining: int
class SubscriptionItem(TypedDict):
id: Snowflake
quantity: int
plan_id: Snowflake
class SubscriptionDiscount(TypedDict):
type: Literal[1, 2, 3, 4]
amount: int
class SubscriptionInvoiceItem(TypedDict):
id: Snowflake
quantity: int
amount: int
proration: bool
subscription_plan_id: Snowflake
subscription_plan_price: int
discounts: List[SubscriptionDiscount]
class SubscriptionInvoice(TypedDict):
id: Snowflake
status: NotRequired[Literal[1, 2, 3, 4]]
currency: str
subtotal: int
tax: int
total: int
tax_inclusive: bool
items: List[SubscriptionInvoiceItem]
current_period_start: str
current_period_end: str
class SubscriptionRenewalMutations(TypedDict, total=False):
payment_gateway_plan_id: Optional[str]
items: List[SubscriptionItem]
class PartialSubscription(TypedDict):
id: Snowflake
type: Literal[1, 2, 3]
payment_gateway: Optional[Literal[1, 2, 3, 4, 5, 6]]
currency: str
items: List[SubscriptionItem]
payment_gateway_plan_id: Optional[str]
payment_gateway_subscription_id: NotRequired[Optional[str]]
current_period_start: str
current_period_end: str
streak_started_at: NotRequired[str]
class Subscription(PartialSubscription):
status: Literal[0, 1, 2, 3, 4, 5, 6]
renewal_mutations: NotRequired[SubscriptionRenewalMutations]
trial_id: NotRequired[Snowflake]
payment_source_id: Optional[Snowflake]
created_at: str
canceled_at: NotRequired[str]
trial_ends_at: NotRequired[str]
metadata: NotRequired[Dict[str, Any]]
latest_invoice: NotRequired[SubscriptionInvoice]
class SubscriptionTrial(TypedDict):
id: Snowflake
interval: Literal[1, 2, 3]
interval_count: int
sku_id: Snowflake
class SubscriptionPrice(TypedDict):
currency: str
amount: int
exponent: int
class SubscriptionCountryPrice(TypedDict):
country_code: str
prices: List[SubscriptionPrice]
class SubscriptionPrices(TypedDict):
country_prices: SubscriptionCountryPrice
payment_source_prices: Dict[Snowflake, List[SubscriptionPrice]]
class PartialSubscriptionPlan(TypedDict):
id: Snowflake
name: str
sku_id: Snowflake
interval: Literal[1, 2, 3]
interval_count: int
tax_inclusive: bool
class SubscriptionPlan(PartialSubscriptionPlan):
prices: Dict[Literal[0, 1, 2, 3, 4], SubscriptionPrices]
price_tier: Literal[None]
currency: str
price: int
discount_price: NotRequired[int]
fallback_currency: NotRequired[str]
fallback_price: NotRequired[int]
fallback_discount_price: NotRequired[int]

22
discord/types/team.py

@ -24,14 +24,15 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
from typing import TypedDict, List, Optional
from typing import Literal, TypedDict, List, Optional
from typing_extensions import NotRequired
from .user import User
from .user import PartialUser
from .snowflake import Snowflake
class TeamMember(TypedDict):
user: User
user: PartialUser
membership_state: int
permissions: List[str]
team_id: Snowflake
@ -41,5 +42,18 @@ class Team(TypedDict):
id: Snowflake
name: str
owner_user_id: Snowflake
members: List[TeamMember]
icon: Optional[str]
payout_account_status: NotRequired[Optional[Literal[1, 2, 3, 4, 5, 6]]]
stripe_connect_account_id: NotRequired[Optional[str]]
members: NotRequired[List[TeamMember]]
class TeamPayout(TypedDict):
id: Snowflake
user_id: Snowflake
amount: int
status: Literal[1, 2, 3, 4, 5]
period_start: str
period_end: Optional[str]
payout_date: Optional[str]
latest_tipalti_submission_response: NotRequired[dict]

12
discord/types/user.py

@ -34,6 +34,10 @@ class PartialUser(TypedDict):
username: str
discriminator: str
avatar: Optional[str]
avatar_decoration: NotRequired[Optional[str]]
public_flags: NotRequired[int]
bot: NotRequired[bool]
system: NotRequired[bool]
ConnectionType = Literal[
@ -60,12 +64,10 @@ ConnectionType = Literal[
'xbox',
]
ConnectionVisibilty = Literal[0, 1]
PremiumType = Literal[0, 1, 2]
PremiumType = Literal[0, 1, 2, 3]
class User(PartialUser, total=False):
bot: bool
system: bool
mfa_enabled: bool
locale: str
verified: bool
@ -74,8 +76,6 @@ class User(PartialUser, total=False):
purchased_flags: int
premium_usage_flags: int
premium_type: PremiumType
public_flags: int
avatar_decoration: Optional[str]
banner: Optional[str]
accent_color: Optional[int]
bio: str
@ -90,6 +90,7 @@ class PartialConnection(TypedDict):
type: ConnectionType
name: str
verified: bool
metadata: NotRequired[Dict[str, Any]]
class Connection(PartialConnection):
@ -101,7 +102,6 @@ class Connection(PartialConnection):
two_way_link: bool
integrations: NotRequired[List[ConnectionIntegration]]
access_token: NotRequired[str]
metadata: NotRequired[Dict[str, Any]]
class ConnectionAccessToken(TypedDict):

45
discord/user.py

@ -312,7 +312,7 @@ class BaseUser(_UserTag):
return self
def _to_minimal_user_json(self) -> UserPayload:
def _to_minimal_user_json(self) -> PartialUserPayload:
user: UserPayload = {
'username': self.name,
'id': self.id,
@ -534,8 +534,6 @@ class ClientUser(BaseUser):
The phone number of the user.
.. versionadded:: 1.9
locale: Optional[:class:`Locale`]
The IETF language tag used to identify the language the user is using.
mfa_enabled: :class:`bool`
Specifies if the user has MFA turned on and working.
premium_type: Optional[:class:`PremiumType`]
@ -548,12 +546,20 @@ class ClientUser(BaseUser):
Specifies if the user should be allowed to access NSFW content.
If ``None``, then the user's date of birth is not known.
.. versionadded:: 2.0
desktop: :class:`bool`
Specifies whether the user has used a desktop client.
.. versionadded:: 2.0
mobile: :class:`bool`
Specifies whether the user has used a mobile client.
.. versionadded:: 2.0
"""
__slots__ = (
'__weakref__',
'locale',
'_locale',
'_flags',
'verified',
'mfa_enabled',
@ -563,6 +569,8 @@ class ClientUser(BaseUser):
'note',
'bio',
'nsfw_allowed',
'desktop',
'mobile',
'_purchased_flags',
'_premium_usage_flags',
)
@ -571,7 +579,7 @@ class ClientUser(BaseUser):
verified: bool
email: Optional[str]
phone: Optional[int]
locale: Locale
_locale: str
_flags: int
mfa_enabled: bool
premium_type: Optional[PremiumType]
@ -594,14 +602,16 @@ class ClientUser(BaseUser):
self.verified = data.get('verified', False)
self.email = data.get('email')
self.phone = _get_as_snowflake(data, 'phone')
self.locale = try_enum(Locale, data.get('locale', 'en-US'))
self._locale = data.get('locale', 'en-US')
self._flags = data.get('flags', 0)
self._purchased_flags = data.get('purchased_flags', 0)
self._premium_usage_flags = data.get('premium_usage_flags', 0)
self.mfa_enabled = data.get('mfa_enabled', False)
self.premium_type = try_enum(PremiumType, data['premium_type']) if 'premium_type' in data else None
self.premium_type = try_enum(PremiumType, data.get('premium_type')) if data.get('premium_type') else None
self.bio = data.get('bio') or None
self.nsfw_allowed = data.get('nsfw_allowed')
self.desktop: bool = data.get('desktop', False)
self.mobile: bool = data.get('mobile', False)
def get_relationship(self, user_id: int) -> Optional[Relationship]:
"""Retrieves the :class:`Relationship` if applicable.
@ -618,6 +628,11 @@ class ClientUser(BaseUser):
"""
return self._state._relationships.get(user_id)
@property
def locale(self) -> Locale:
""":class:`Locale`: The IETF language tag used to identify the language the user is using."""
return self.settings.locale if self.settings else try_enum(Locale, self._locale)
@property
def premium(self) -> bool:
"""Indicates if the user is a premium user (i.e. has Discord Nitro)."""
@ -687,7 +702,7 @@ class ClientUser(BaseUser):
*,
username: str = MISSING,
avatar: Optional[bytes] = MISSING,
avatar_decoration: Optional[bool] = MISSING,
avatar_decoration: Optional[bytes] = MISSING,
password: str = MISSING,
new_password: str = MISSING,
email: str = MISSING,
@ -737,7 +752,7 @@ class ClientUser(BaseUser):
avatar: Optional[:class:`bytes`]
A :term:`py:bytes-like object` representing the image to upload.
Could be ``None`` to denote no avatar.
avatar_decoration: Optional[:class:`bool`]
avatar_decoration: Optional[:class:`bytes`]
A :term:`py:bytes-like object` representing the image to upload.
Could be ``None`` to denote no avatar decoration.
@ -747,12 +762,18 @@ class ClientUser(BaseUser):
Could be ``None`` to denote no banner.
accent_colour: :class:`Colour`
A :class:`Colour` object of the colour you want to set your profile to.
.. versionadded:: 2.0
bio: :class:`str`
Your "about me" section.
Could be ``None`` to represent no bio.
.. versionadded:: 2.0
date_of_birth: :class:`datetime.datetime`
Your date of birth. Can only ever be set once.
.. versionadded:: 2.0
Raises
------
HTTPException
@ -781,6 +802,12 @@ class ClientUser(BaseUser):
else:
args['avatar'] = None
if avatar_decoration is not MISSING:
if avatar_decoration is not None:
args['avatar_decoration'] = _bytes_to_base64_data(avatar_decoration)
else:
args['avatar_decoration'] = None
if banner is not MISSING:
if banner is not None:
args['banner'] = _bytes_to_base64_data(banner)

107
discord/utils.py

@ -79,6 +79,8 @@ except ModuleNotFoundError:
else:
HAS_ORJSON = True
from .enums import Locale, try_enum
__all__ = (
'oauth_url',
@ -145,6 +147,7 @@ if TYPE_CHECKING:
from .message import Message
from .template import Template
from .commands import ApplicationCommand
from .entitlements import Gift
class _RequestLike(Protocol):
headers: Mapping[str, Any]
@ -260,6 +263,27 @@ def parse_time(timestamp: Optional[str]) -> Optional[datetime.datetime]:
return None
@overload
def parse_date(date: None) -> None:
...
@overload
def parse_date(date: str) -> datetime.date:
...
@overload
def parse_date(date: Optional[str]) -> Optional[datetime.date]:
...
def parse_date(date: Optional[str]) -> Optional[datetime.date]:
if date:
return parse_time(date).date()
return None
def copy_doc(original: Callable) -> Callable[[T], T]:
def decorator(overridden: T) -> T:
overridden.__doc__ = original.__doc__
@ -574,7 +598,7 @@ def _get_as_snowflake(data: Any, key: str) -> Optional[int]:
return value and int(value)
def _get_mime_type_for_image(data: bytes):
def _get_mime_type_for_image(data: bytes, with_video: bool = False, fallback: bool = False) -> str:
if data.startswith(b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A'):
return 'image/png'
elif data[0:3] == b'\xff\xd8\xff' or data[6:10] in (b'JFIF', b'Exif'):
@ -583,13 +607,30 @@ def _get_mime_type_for_image(data: bytes):
return 'image/gif'
elif data.startswith(b'RIFF') and data[8:12] == b'WEBP':
return 'image/webp'
elif data.startswith(b'\x66\x74\x79\x70\x69\x73\x6F\x6D') and with_video:
return 'video/mp4'
else:
if fallback:
return 'application/octet-stream'
raise ValueError('Unsupported image type given')
def _get_extension_for_mime_type(mime_type: str) -> str:
if mime_type == 'image/png':
return 'png'
elif mime_type == 'image/jpeg':
return 'jpg'
elif mime_type == 'image/gif':
return 'gif'
elif mime_type == 'video/mp4':
return 'mp4'
else:
return 'webp'
def _bytes_to_base64_data(data: bytes) -> str:
fmt = 'data:{mime};base64,{data}'
mime = _get_mime_type_for_image(data)
mime = _get_mime_type_for_image(data, fallback=True)
b64 = b64encode(data).decode('ascii')
return fmt.format(mime=mime, data=b64)
@ -598,17 +639,24 @@ def _is_submodule(parent: str, child: str) -> bool:
return parent == child or child.startswith(parent + '.')
def _handle_metadata(obj):
try:
return dict(obj)
except Exception:
raise TypeError(f'Type {obj.__class__.__name__} is not JSON serializable')
if HAS_ORJSON:
def _to_json(obj: Any) -> str:
return orjson.dumps(obj).decode('utf-8')
return orjson.dumps(obj, default=_handle_metadata).decode('utf-8')
_from_json = orjson.loads # type: ignore
else:
def _to_json(obj: Any) -> str:
return json.dumps(obj, separators=(',', ':'), ensure_ascii=True)
return json.dumps(obj, separators=(',', ':'), ensure_ascii=True, default=_handle_metadata)
_from_json = json.loads
@ -824,6 +872,34 @@ def resolve_template(code: Union[Template, str]) -> str:
return code
def resolve_gift(code: Union[Gift, str]) -> str:
"""
Resolves a gift code from a :class:`~discord.Gift`, URL or code.
.. versionadded:: 2.0
Parameters
-----------
code: Union[:class:`~discord.Gift`, :class:`str`]
The code.
Returns
--------
:class:`str`
The gift code.
"""
from .entitlements import Gift # circular import
if isinstance(code, Gift):
return code.code
else:
rx = r'(?:https?\:\/\/)?(?:discord(?:app)?\.com\/(?:gifts|billing\/promotions)|promos\.discord\.gg|discord.gift)\/(.+)'
m = re.match(rx, code)
if m:
return m.group(1)
return code
_MARKDOWN_ESCAPE_SUBREGEX = '|'.join(r'\{0}(?=([\s\S]*((?<!\{0})\{0})))'.format(c) for c in ('*', '`', '_', '~', '|'))
_MARKDOWN_ESCAPE_COMMON = r'^>(?:>>)?\s|\[.+\]\(.+\)'
@ -1195,11 +1271,10 @@ def set_target(
for item in items:
for k, v in attrs.items():
if v is not None:
try:
setattr(item, k, v)
except AttributeError:
pass
try:
setattr(item, k, v)
except AttributeError:
pass
def _generate_session_id() -> str:
@ -1210,6 +1285,16 @@ def _generate_nonce() -> str:
return str(time_snowflake(utcnow()))
def _parse_localizations(data: Any, key: str) -> tuple[Any, dict]:
values = data.get(key)
values = values if isinstance(values, dict) else {'default': values}
string = values['default']
localizations = {
try_enum(Locale, k): v for k, v in (values.get('localizations', data.get(f'{key}_localizations')) or {}).items()
}
return string, localizations
class ExpiringString(collections.UserString):
def __init__(self, data: str, timeout: int) -> None:
super().__init__(data)
@ -1271,7 +1356,9 @@ async def _get_user_agent(session: ClientSession) -> str:
return response[0]
except asyncio.TimeoutError:
_log.critical('Could not fetch user-agent. Falling back to hardcoded value...')
return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36'
return (
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36'
)
def _get_browser_version(user_agent: str) -> str:

2251
docs/api.rst

File diff suppressed because it is too large

54
docs/migrating.rst

@ -263,24 +263,6 @@ In addition to this, :class:`Emoji` and :class:`PartialEmoji` now also share an
The following were affected by this change:
- :attr:`AppInfo.cover_image`
- ``AppInfo.cover_image`` (replaced by :attr:`AppInfo.cover_image.key <Asset.key>`)
- ``AppInfo.cover_image_url`` (replaced by :attr:`AppInfo.cover_image`)
- The new attribute may now be ``None``.
- ``AppInfo.cover_image_url_as`` (replaced by :meth:`AppInfo.cover_image.replace <Asset.replace>`)
- :attr:`AppInfo.icon`
- ``AppInfo.icon`` (replaced by :attr:`AppInfo.icon.key <Asset.key>`)
- ``AppInfo.icon_url`` (replaced by :attr:`AppInfo.icon`)
- The new attribute may now be ``None``.
- ``AppInfo.icon_url_as`` (replaced by :meth:`AppInfo.icon.replace <Asset.replace>`)
- :class:`AuditLogDiff`
- :attr:`AuditLogDiff.avatar` is now of :class:`Asset` type.
@ -1028,17 +1010,13 @@ The following have been removed:
- There is no replacement for this one. The current API version no longer provides enough data for this to be possible.
- ``AppInfo.summary``
- There is no replacement for this one. The current API version no longer provides this field.
- ``User.permissions_in`` and ``Member.permissions_in``
- Use :meth:`abc.GuildChannel.permissions_for` instead.
- ``guild_subscriptions`` parameter from :class:`Client` constructor
- The current API version no longer provides this functionality. Use ``intents`` parameter instead.
- The current API version no longer provides this functionality.
- ``guild_subscription_options`` parameter from :class:`Client` constructor
@ -1068,13 +1046,6 @@ The following have been removed:
- ``region`` parameter from :meth:`Client.create_guild`
- ``region`` parameter from :meth:`Template.create_guild`
- ``region`` parameter from :meth:`Guild.edit`
- ``on_private_channel_create`` event
- Discord API no longer sends channel create event for DMs.
- ``on_private_channel_delete`` event
- Discord API no longer sends channel create event for DMs.
- The undocumented private ``on_socket_response`` event
@ -1092,29 +1063,6 @@ The following changes have been made:
- :func:`utils.resolve_invite` now returns a :class:`ResolvedInvite` class.
- :func:`utils.oauth_url` now defaults to ``bot`` and ``applications.commands`` scopes when not given instead of just ``bot``.
- :meth:`abc.Messageable.typing` can no longer be used as a regular (non-async) context manager.
- :attr:`Intents.emojis` is now an alias of :attr:`Intents.emojis_and_stickers`.
This may affect code that iterates through ``(name, value)`` pairs in an instance of this class:
.. code:: python
# before
friendly_names = {
...,
'emojis': 'Emojis Intent',
...,
}
for name, value in discord.Intents.all():
print(f'{friendly_names[name]}: {value}')
# after
friendly_names = {
...,
'emojis_and_stickers': 'Emojis Intent',
...,
}
for name, value in discord.Intents.all():
print(f'{friendly_names[name]}: {value}')
- ``created_at`` is no longer part of :class:`abc.Snowflake`.

1
pyproject.toml

@ -41,6 +41,7 @@ exclude = [
]
reportUnnecessaryTypeIgnoreComment = "warning"
reportUnusedImport = "error"
reportShadowedImports = false
pythonVersion = "3.8"
typeCheckingMode = "basic"

Loading…
Cancel
Save