Browse Source

Rewrite Asset design

This is a breaking change.

This does the following transformations, assuming `asset` represents
an asset type.

Object.is_asset_animated() => Object.asset.is_animated()
Object.asset => Object.asset.key
Object.asset_url => Object.asset_url
Object.asset_url_as => Object.asset.replace(...)

Since the asset type now requires a key (or hash, if you will),
Emoji had to be flattened similar to how Attachment was done since
these assets are keyed solely ID.

Emoji.url (Asset) => Emoji.url (str)
Emoji.url_as => removed
Emoji.url.read => Emoji.read
Emoji.url.save => Emoji.save

This transformation was also done to PartialEmoji.
pull/6744/head
Rapptz 4 years ago
parent
commit
9eaf1e85e4
  1. 98
      discord/appinfo.py
  2. 339
      discord/asset.py
  3. 57
      discord/channel.py
  4. 104
      discord/emoji.py
  5. 2
      discord/flags.py
  6. 179
      discord/guild.py
  7. 75
      discord/invite.py
  8. 6
      discord/member.py
  9. 4
      discord/message.py
  10. 96
      discord/partial_emoji.py
  11. 35
      discord/sticker.py
  12. 47
      discord/team.py
  13. 68
      discord/user.py
  14. 104
      discord/webhook/async_.py

98
discord/appinfo.py

@ -49,8 +49,6 @@ class AppInfo:
.. versionadded:: 1.3 .. versionadded:: 1.3
icon: Optional[:class:`str`]
The icon hash, if it exists.
description: Optional[:class:`str`] description: Optional[:class:`str`]
The application description. The application description.
bot_public: :class:`bool` bot_public: :class:`bool`
@ -88,12 +86,6 @@ class AppInfo:
If this application is a game sold on Discord, If this application is a game sold on Discord,
this field will be the URL slug that links to the store page this field will be the URL slug that links to the store page
.. versionadded:: 1.3
cover_image: Optional[:class:`str`]
If this application is a game sold on Discord,
this field will be the hash of the image on store embeds
.. versionadded:: 1.3 .. versionadded:: 1.3
""" """
@ -106,14 +98,14 @@ class AppInfo:
'bot_public', 'bot_public',
'bot_require_code_grant', 'bot_require_code_grant',
'owner', 'owner',
'icon', '_icon',
'summary', 'summary',
'verify_key', 'verify_key',
'team', 'team',
'guild_id', 'guild_id',
'primary_sku_id', 'primary_sku_id',
'slug', 'slug',
'cover_image', '_cover_image',
) )
def __init__(self, state, data): def __init__(self, state, data):
@ -122,7 +114,7 @@ class AppInfo:
self.id = int(data['id']) self.id = int(data['id'])
self.name = data['name'] self.name = data['name']
self.description = data['description'] self.description = data['description']
self.icon = data['icon'] self._icon = data['icon']
self.rpc_origins = data['rpc_origins'] self.rpc_origins = data['rpc_origins']
self.bot_public = data['bot_public'] self.bot_public = data['bot_public']
self.bot_require_code_grant = data['bot_require_code_grant'] self.bot_require_code_grant = data['bot_require_code_grant']
@ -138,7 +130,7 @@ class AppInfo:
self.primary_sku_id = utils._get_as_snowflake(data, 'primary_sku_id') self.primary_sku_id = utils._get_as_snowflake(data, 'primary_sku_id')
self.slug = data.get('slug') self.slug = data.get('slug')
self.cover_image = data.get('cover_image') self._cover_image = data.get('cover_image')
def __repr__(self): def __repr__(self):
return ( return (
@ -148,81 +140,21 @@ class AppInfo:
) )
@property @property
def icon_url(self): def icon(self):
""":class:`.Asset`: Retrieves the application's icon asset. """Optional[:class:`.Asset`]: Retrieves the application's icon asset, if any."""
if self._icon is None:
This is equivalent to calling :meth:`icon_url_as` with return None
the default parameters ('webp' format and a size of 1024). return Asset._from_icon(self._state, self.id, self._icon, path='app')
.. versionadded:: 1.3
"""
return self.icon_url_as()
def icon_url_as(self, *, format='webp', size=1024):
"""Returns an :class:`Asset` for the icon the application has.
The format must be one of 'webp', 'jpeg', 'jpg' or 'png'.
The size must be a power of 2 between 16 and 4096.
.. versionadded:: 1.6
Parameters
-----------
format: :class:`str`
The format to attempt to convert the icon to. Defaults to 'webp'.
size: :class:`int`
The size of the image to display.
Raises
------
InvalidArgument
Bad image format passed to ``format`` or invalid ``size``.
Returns
--------
:class:`Asset`
The resulting CDN asset.
"""
return Asset._from_icon(self._state, self, 'app', format=format, size=size)
@property @property
def cover_image_url(self): def cover_image(self):
""":class:`.Asset`: Retrieves the cover image on a store embed. """Optional[:class:`.Asset`]: Retrieves the cover image on a store embed, if any.
This is equivalent to calling :meth:`cover_image_url_as` with
the default parameters ('webp' format and a size of 1024).
.. versionadded:: 1.3
"""
return self.cover_image_url_as()
def cover_image_url_as(self, *, format='webp', size=1024):
"""Returns an :class:`Asset` for the image on store embeds
if this application is a game sold on Discord.
The format must be one of 'webp', 'jpeg', 'jpg' or 'png'.
The size must be a power of 2 between 16 and 4096.
.. versionadded:: 1.6
Parameters
-----------
format: :class:`str`
The format to attempt to convert the image to. Defaults to 'webp'.
size: :class:`int`
The size of the image to display.
Raises
------
InvalidArgument
Bad image format passed to ``format`` or invalid ``size``.
Returns This is only available if the application is a game sold on Discord.
--------
:class:`Asset`
The resulting CDN asset.
""" """
return Asset._from_cover_image(self._state, self, format=format, size=size) if self._cover_image is None:
return None
return Asset._from_cover_image(self._state, self.id, self._cover_image)
@property @property
def guild(self): def guild(self):

339
discord/asset.py

@ -22,22 +22,28 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE. DEALINGS IN THE SOFTWARE.
""" """
from __future__ import annotations
import io import io
from typing import Literal, TYPE_CHECKING import os
from typing import BinaryIO, Literal, TYPE_CHECKING, Tuple, Union
from .errors import DiscordException from .errors import DiscordException
from .errors import InvalidArgument from .errors import InvalidArgument
from . import utils from . import utils
import yarl
__all__ = ( __all__ = (
'Asset', 'Asset',
) )
if TYPE_CHECKING: if TYPE_CHECKING:
ValidStaticFormatTypes = Literal['webp', 'jpeg', 'jpg', 'png'] ValidStaticFormatTypes = Literal['webp', 'jpeg', 'jpg', 'png']
ValidAvatarFormatTypes = Literal['webp', 'jpeg', 'jpg', 'png', 'gif'] ValidAssetFormatTypes = Literal['webp', 'jpeg', 'jpg', 'png', 'gif']
VALID_STATIC_FORMATS = frozenset({"jpeg", "jpg", "webp", "png"}) VALID_STATIC_FORMATS = frozenset({"jpeg", "jpg", "webp", "png"})
VALID_AVATAR_FORMATS = VALID_STATIC_FORMATS | {"gif"} VALID_ASSET_FORMATS = VALID_STATIC_FORMATS | {"gif"}
class Asset: class Asset:
"""Represents a CDN asset on Discord. """Represents a CDN asset on Discord.
@ -52,10 +58,6 @@ class Asset:
Returns the length of the CDN asset's URL. Returns the length of the CDN asset's URL.
.. describe:: bool(x)
Checks if the Asset has a URL.
.. describe:: x == y .. describe:: x == y
Checks if the asset is equal to another asset. Checks if the asset is equal to another asset.
@ -68,96 +70,88 @@ class Asset:
Returns the hash of the asset. Returns the hash of the asset.
""" """
__slots__ = ('_state', '_url')
__slots__: Tuple[str, ...] = (
'_state',
'_url',
'_animated',
'_key',
)
BASE = 'https://cdn.discordapp.com' BASE = 'https://cdn.discordapp.com'
def __init__(self, state, url=None): def __init__(self, state, *, url: str, key: str, animated: bool = False):
self._state = state self._state = state
self._url = url self._url = url
self._animated = animated
self._key = key
@classmethod @classmethod
def _from_avatar(cls, state, user, *, format=None, static_format='webp', size=1024): def _from_default_avatar(cls, state, index: int) -> Asset:
if not utils.valid_icon_size(size): return cls(
raise InvalidArgument("size must be a power of 2 between 16 and 4096") state,
if format is not None and format not in VALID_AVATAR_FORMATS: url=f'{cls.BASE}/embed/avatars/{index}.png',
raise InvalidArgument(f"format must be None or one of {VALID_AVATAR_FORMATS}") key=str(index),
if format == "gif" and not user.is_avatar_animated(): animated=False,
raise InvalidArgument("non animated avatars do not support gif format") )
if static_format not in VALID_STATIC_FORMATS:
raise InvalidArgument(f"static_format must be one of {VALID_STATIC_FORMATS}")
if user.avatar is None:
return user.default_avatar_url
if format is None:
format = 'gif' if user.is_avatar_animated() else static_format
return cls(state, f'/avatars/{user.id}/{user.avatar}.{format}?size={size}')
@classmethod @classmethod
def _from_icon(cls, state, object, path, *, format='webp', size=1024): def _from_avatar(cls, state, user_id: int, avatar: str) -> Asset:
if object.icon is None: animated = avatar.startswith('a_')
return cls(state) format = 'gif' if animated else 'png'
return cls(
if not utils.valid_icon_size(size): state,
raise InvalidArgument("size must be a power of 2 between 16 and 4096") url=f'{cls.BASE}/avatars/{user_id}/{avatar}.{format}?size=1024',
if format not in VALID_STATIC_FORMATS: key=avatar,
raise InvalidArgument(f"format must be None or one of {VALID_STATIC_FORMATS}") animated=animated,
)
url = f'/{path}-icons/{object.id}/{object.icon}.{format}?size={size}'
return cls(state, url)
@classmethod @classmethod
def _from_cover_image(cls, state, obj, *, format='webp', size=1024): def _from_icon(cls, state, object_id: int, icon_hash: str, path: str) -> Asset:
if obj.cover_image is None: return cls(
return cls(state) state,
url=f'{cls.BASE}/{path}-icons/{object_id}/{icon_hash}.png?size=1024',
if not utils.valid_icon_size(size): key=icon_hash,
raise InvalidArgument("size must be a power of 2 between 16 and 4096") animated=False,
if format not in VALID_STATIC_FORMATS: )
raise InvalidArgument(f"format must be None or one of {VALID_STATIC_FORMATS}")
url = f'/app-assets/{obj.id}/store/{obj.cover_image}.{format}?size={size}'
return cls(state, url)
@classmethod @classmethod
def _from_guild_image(cls, state, id, hash, key, *, format='webp', size=1024): def _from_cover_image(cls, state, object_id: int, cover_image_hash: str) -> Asset:
if not utils.valid_icon_size(size): return cls(
raise InvalidArgument("size must be a power of 2 between 16 and 4096") state,
if format not in VALID_STATIC_FORMATS: url=f'{cls.BASE}/app-assets/{object_id}/store/{cover_image_hash}.png?size=1024',
raise InvalidArgument(f"format must be one of {VALID_STATIC_FORMATS}") key=cover_image_hash,
animated=False,
if hash is None: )
return cls(state)
return cls(state, f'/{key}/{id}/{hash}.{format}?size={size}')
@classmethod @classmethod
def _from_guild_icon(cls, state, guild, *, format=None, static_format='webp', size=1024): def _from_guild_image(cls, state, guild_id: int, image: str, path: str) -> Asset:
if not utils.valid_icon_size(size): return cls(
raise InvalidArgument("size must be a power of 2 between 16 and 4096") state,
if format is not None and format not in VALID_AVATAR_FORMATS: url=f'{cls.BASE}/{path}/{guild_id}/{image}.png?size=1024',
raise InvalidArgument(f"format must be one of {VALID_AVATAR_FORMATS}") key=image,
if format == "gif" and not guild.is_icon_animated(): animated=False,
raise InvalidArgument("non animated guild icons do not support gif format") )
if static_format not in VALID_STATIC_FORMATS:
raise InvalidArgument(f"static_format must be one of {VALID_STATIC_FORMATS}")
if guild.icon is None:
return cls(state)
if format is None:
format = 'gif' if guild.is_icon_animated() else static_format
return cls(state, f'/icons/{guild.id}/{guild.icon}.{format}?size={size}')
@classmethod @classmethod
def _from_sticker_url(cls, state, sticker, *, size=1024): def _from_guild_icon(cls, state, guild_id: int, icon_hash: str) -> Asset:
if not utils.valid_icon_size(size): animated = icon_hash.startswith('a_')
raise InvalidArgument("size must be a power of 2 between 16 and 4096") format = 'gif' if animated else 'png'
return cls(
state,
url=f'{cls.BASE}/icons/{guild_id}/{icon_hash}.{format}?size=1024',
key=icon_hash,
animated=animated,
)
return cls(state, f'/stickers/{sticker.id}/{sticker.image}.png?size={size}') @classmethod
def _from_sticker(cls, state, sticker_id: int, sticker_hash: str) -> Asset:
return cls(
state,
url=f'{cls.BASE}/stickers/{sticker_id}/{sticker_hash}.png?size=1024',
key=sticker_hash,
animated=False,
)
@classmethod @classmethod
def _from_emoji(cls, state, emoji, *, format=None, static_format='png'): def _from_emoji(cls, state, emoji, *, format=None, static_format='png'):
@ -172,46 +166,184 @@ class Asset:
return cls(state, f'/emojis/{emoji.id}.{format}') return cls(state, f'/emojis/{emoji.id}.{format}')
def __str__(self): def __str__(self) -> str:
return self.BASE + self._url if self._url is not None else '' return self._url
def __len__(self):
if self._url:
return len(self.BASE + self._url)
return 0
def __bool__(self): def __len__(self) -> int:
return self._url is not None return len(self._url)
def __repr__(self): def __repr__(self):
return f'<Asset url={self._url!r}>' shorten = self._url.replace(self.BASE, '')
return f'<Asset url={shorten!r}>'
def __eq__(self, other): def __eq__(self, other):
return isinstance(other, Asset) and self._url == other._url return isinstance(other, Asset) and self._url == other._url
def __ne__(self, other):
return not self.__eq__(other)
def __hash__(self): def __hash__(self):
return hash(self._url) return hash(self._url)
async def read(self): @property
"""|coro| def url(self) -> str:
""":class:`str`: Returns the underlying URL of the asset."""
return self._url
Retrieves the content of this asset as a :class:`bytes` object. @property
def key(self) -> str:
""":class:`str`: Returns the identifying key of the asset."""
return self._key
.. warning:: def is_animated(self) -> bool:
""":class:`bool`: Returns whether the asset is animated."""
return self._animated
:class:`PartialEmoji` won't have a connection state if user created, def replace(
and a URL won't be present if a custom image isn't associated with self,
the asset, e.g. a guild with no custom icon. size: int = ...,
format: ValidAssetFormatTypes = ...,
static_format: ValidStaticFormatTypes = ...,
) -> Asset:
"""Returns a new asset with the passed components replaced.
Parameters
-----------
size: :class:`int`
The new size of the asset.
format: :class:`str`
The new format to change it to. Must be either
'webp', 'jpeg', 'jpg', 'png', or 'gif' if it's animated.
static_format: :class:`str`
The new format to change it to if the asset isn't animated.
Must be either 'webp', 'jpeg', 'jpg', or 'png'.
Raises
-------
InvalidArgument
An invalid size or format was passed.
Returns
--------
:class:`Asset`
The newly updated asset.
"""
url = yarl.URL(self._url)
path, _ = os.path.splitext(url.path)
if format is not ...:
if self._animated:
if format not in VALID_ASSET_FORMATS:
raise InvalidArgument(f'format must be one of {VALID_ASSET_FORMATS}')
else:
if format not in VALID_STATIC_FORMATS:
raise InvalidArgument(f'format must be one of {VALID_STATIC_FORMATS}')
url = url.with_path(f'{path}.{format}')
if static_format is not ... and not self._animated:
if static_format not in VALID_STATIC_FORMATS:
raise InvalidArgument(f'static_format must be one of {VALID_STATIC_FORMATS}')
url = url.with_path(f'{path}.{static_format}')
if size is not ...:
if not utils.valid_icon_size(size):
raise InvalidArgument('size must be a power of 2 between 16 and 4096')
url = url.with_query(size=size)
else:
url = url.with_query(url.raw_query_string)
url = str(url)
return Asset(state=self._state, url=url, key=self._key, animated=self._animated)
def with_size(self, size: int) -> Asset:
"""Returns a new asset with the specified size.
Parameters
------------
size: :class:`int`
The new size of the asset.
Raises
-------
InvalidArgument
The asset had an invalid size.
Returns
--------
:class:`Asset`
The new updated asset.
"""
if not utils.valid_icon_size(size):
raise InvalidArgument('size must be a power of 2 between 16 and 4096')
url = str(yarl.URL(self._url).with_query(size=size))
return Asset(state=self._state, url=url, key=self._key, animated=self._animated)
def with_format(self, format: ValidAssetFormatTypes) -> Asset:
"""Returns a new asset with the specified format.
Parameters
------------
format: :class:`str`
The new format of the asset.
Raises
-------
InvalidArgument
The asset had an invalid format.
Returns
--------
:class:`Asset`
The new updated asset.
"""
if self._animated:
if format not in VALID_ASSET_FORMATS:
raise InvalidArgument(f'format must be one of {VALID_ASSET_FORMATS}')
else:
if format not in VALID_STATIC_FORMATS:
raise InvalidArgument(f'format must be one of {VALID_STATIC_FORMATS}')
url = yarl.URL(self._url)
path, _ = os.path.splitext(url.path)
url = str(url.with_path(f'{path}.{format}').with_query(url.raw_query_string))
return Asset(state=self._state, url=url, key=self._key, animated=self._animated)
def with_static_format(self, format: ValidStaticFormatTypes) -> Asset:
"""Returns a new asset with the specified static format.
This only changes the format if the underlying asset is
not animated. Otherwise, the asset is not changed.
Parameters
------------
format: :class:`str`
The new static format of the asset.
Raises
-------
InvalidArgument
The asset had an invalid format.
Returns
--------
:class:`Asset`
The new updated asset.
"""
if self._animated:
return self
return self.with_format(format)
async def read(self) -> bytes:
"""|coro|
Retrieves the content of this asset as a :class:`bytes` object.
.. versionadded:: 1.1 .. versionadded:: 1.1
Raises Raises
------ ------
DiscordException DiscordException
There was no valid URL or internal connection state. There was no internal connection state.
HTTPException HTTPException
Downloading the asset failed. Downloading the asset failed.
NotFound NotFound
@ -222,15 +354,12 @@ class Asset:
:class:`bytes` :class:`bytes`
The content of the asset. The content of the asset.
""" """
if not self._url:
raise DiscordException('Invalid asset (no URL provided)')
if self._state is None: if self._state is None:
raise DiscordException('Invalid state (no ConnectionState provided)') raise DiscordException('Invalid state (no ConnectionState provided)')
return await self._state.http.get_from_cdn(self.BASE + self._url) return await self._state.http.get_from_cdn(self.BASE + self._url)
async def save(self, fp, *, seek_begin=True): async def save(self, fp: Union[str, bytes, os.PathLike, BinaryIO], *, seek_begin: bool = True) -> int:
"""|coro| """|coro|
Saves this asset into a file-like object. Saves this asset into a file-like object.
@ -245,7 +374,7 @@ class Asset:
Raises Raises
------ ------
DiscordException DiscordException
There was no valid URL or internal connection state. There was no internal connection state.
HTTPException HTTPException
Downloading the asset failed. Downloading the asset failed.
NotFound NotFound

57
discord/channel.py

@ -94,9 +94,9 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
:attr:`~Permissions.manage_messages` bypass slowmode. :attr:`~Permissions.manage_messages` bypass slowmode.
nsfw: :class:`bool` nsfw: :class:`bool`
If the channel is marked as "not safe for work". If the channel is marked as "not safe for work".
.. note:: .. note::
To check if the channel or the guild of that channel are marked as NSFW, consider :meth:`is_nsfw` instead. To check if the channel or the guild of that channel are marked as NSFW, consider :meth:`is_nsfw` instead.
""" """
@ -894,9 +894,9 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
top category is position 0. top category is position 0.
nsfw: :class:`bool` nsfw: :class:`bool`
If the channel is marked as "not safe for work". If the channel is marked as "not safe for work".
.. note:: .. note::
To check if the channel or the guild of that channel are marked as NSFW, consider :meth:`is_nsfw` instead. To check if the channel or the guild of that channel are marked as NSFW, consider :meth:`is_nsfw` instead.
""" """
@ -1096,9 +1096,9 @@ class StoreChannel(discord.abc.GuildChannel, Hashable):
top channel is position 0. top channel is position 0.
nsfw: :class:`bool` nsfw: :class:`bool`
If the channel is marked as "not safe for work". If the channel is marked as "not safe for work".
.. note:: .. note::
To check if the channel or the guild of that channel are marked as NSFW, consider :meth:`is_nsfw` instead. To check if the channel or the guild of that channel are marked as NSFW, consider :meth:`is_nsfw` instead.
""" """
__slots__ = ('name', 'id', 'guild', '_state', 'nsfw', __slots__ = ('name', 'id', 'guild', '_state', 'nsfw',
@ -1343,13 +1343,11 @@ class GroupChannel(discord.abc.Messageable, Hashable):
The group channel ID. The group channel ID.
owner: :class:`User` owner: :class:`User`
The user that owns the group channel. The user that owns the group channel.
icon: Optional[:class:`str`]
The group channel's icon hash if provided.
name: Optional[:class:`str`] name: Optional[:class:`str`]
The group channel's name if provided. The group channel's name if provided.
""" """
__slots__ = ('id', 'recipients', 'owner', 'icon', 'name', 'me', '_state') __slots__ = ('id', 'recipients', 'owner', '_icon', 'name', 'me', '_state')
def __init__(self, *, me, state, data): def __init__(self, *, me, state, data):
self._state = state self._state = state
@ -1359,7 +1357,7 @@ class GroupChannel(discord.abc.Messageable, Hashable):
def _update_group(self, data): def _update_group(self, data):
owner_id = utils._get_as_snowflake(data, 'owner_id') owner_id = utils._get_as_snowflake(data, 'owner_id')
self.icon = data.get('icon') self._icon = data.get('icon')
self.name = data.get('name') self.name = data.get('name')
try: try:
@ -1393,40 +1391,11 @@ class GroupChannel(discord.abc.Messageable, Hashable):
return ChannelType.group return ChannelType.group
@property @property
def icon_url(self): def icon(self):
""":class:`Asset`: Returns the channel's icon asset if available. """Optional[:class:`Asset`]: Returns the channel's icon asset if available."""
if self._icon is None:
This is equivalent to calling :meth:`icon_url_as` with return None
the default parameters ('webp' format and a size of 1024). return Asset._from_icon(self._state, self.id, self._icon, path='channel')
"""
return self.icon_url_as()
def icon_url_as(self, *, format='webp', size=1024):
"""Returns an :class:`Asset` for the icon the channel has.
The format must be one of 'webp', 'jpeg', 'jpg' or 'png'.
The size must be a power of 2 between 16 and 4096.
.. versionadded:: 2.0
Parameters
-----------
format: :class:`str`
The format to attempt to convert the icon to. Defaults to 'webp'.
size: :class:`int`
The size of the image to display.
Raises
------
InvalidArgument
Bad image format passed to ``format`` or invalid ``size``.
Returns
--------
:class:`Asset`
The resulting CDN asset.
"""
return Asset._from_icon(self._state, self, 'channel', format=format, size=size)
@property @property
def created_at(self): def created_at(self):

104
discord/emoji.py

@ -22,6 +22,8 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE. DEALINGS IN THE SOFTWARE.
""" """
import io
from .asset import Asset from .asset import Asset
from . import utils from . import utils
from .partial_emoji import _EmojiTag from .partial_emoji import _EmojiTag
@ -132,13 +134,10 @@ class Emoji(_EmojiTag):
return utils.snowflake_time(self.id) return utils.snowflake_time(self.id)
@property @property
def url(self): def url(self) -> str:
""":class:`Asset`: Returns the asset of the emoji. """:class:`str`: Returns the URL of the emoji."""
fmt = 'gif' if self.animated else 'png'
This is equivalent to calling :meth:`url_as` with return f'{Asset.BASE}/emojis/{self.id}.{fmt}'
the default parameters (i.e. png/gif detection).
"""
return self.url_as(format=None)
@property @property
def roles(self): def roles(self):
@ -157,39 +156,6 @@ class Emoji(_EmojiTag):
""":class:`Guild`: The guild this emoji belongs to.""" """:class:`Guild`: The guild this emoji belongs to."""
return self._state._get_guild(self.guild_id) return self._state._get_guild(self.guild_id)
def url_as(self, *, format=None, static_format="png"):
"""Returns an :class:`Asset` for the emoji's url.
The format must be one of 'webp', 'jpeg', 'jpg', 'png' or 'gif'.
'gif' is only valid for animated emojis.
.. versionadded:: 1.6
Parameters
-----------
format: Optional[:class:`str`]
The format to attempt to convert the emojis to.
If the format is ``None``, then it is automatically
detected as either 'gif' or static_format, depending on whether the
emoji is animated or not.
static_format: Optional[:class:`str`]
Format to attempt to convert only non-animated emoji's to.
Defaults to 'png'
Raises
-------
InvalidArgument
Bad image format passed to ``format`` or ``static_format``.
Returns
--------
:class:`Asset`
The resulting CDN asset.
"""
return Asset._from_emoji(self._state, self, format=format, static_format=static_format)
def is_usable(self): def is_usable(self):
""":class:`bool`: Whether the bot can use this emoji. """:class:`bool`: Whether the bot can use this emoji.
@ -254,3 +220,61 @@ class Emoji(_EmojiTag):
if roles: if roles:
roles = [role.id for role in roles] roles = [role.id for role in roles]
await self._state.http.edit_custom_emoji(self.guild.id, self.id, name=name, roles=roles, reason=reason) await self._state.http.edit_custom_emoji(self.guild.id, self.id, name=name, roles=roles, reason=reason)
async def read(self):
"""|coro|
Retrieves the content of this emoji as a :class:`bytes` object.
.. versionadded:: 2.0
Raises
------
HTTPException
Downloading the emoji failed.
NotFound
The emoji was deleted.
Returns
-------
:class:`bytes`
The content of the emoji.
"""
return await self._state.http.get_from_cdn(self.url)
async def save(self, fp, *, seek_begin=True):
"""|coro|
Saves this emoji into a file-like object.
.. versionadded:: 2.0
Parameters
----------
fp: Union[BinaryIO, :class:`os.PathLike`]
Same as in :meth:`Attachment.save`.
seek_begin: :class:`bool`
Same as in :meth:`Attachment.save`.
Raises
------
HTTPException
Downloading the emoji failed.
NotFound
The emoji was deleted.
Returns
--------
:class:`int`
The number of bytes written.
"""
data = await self.read()
if isinstance(fp, io.IOBase) and fp.writable():
written = fp.write(data)
if seek_begin:
fp.seek(0)
return written
else:
with open(fp, 'wb') as f:
return f.write(data)

2
discord/flags.py

@ -504,7 +504,7 @@ class Intents(BaseFlags):
- :attr:`Member.nick` - :attr:`Member.nick`
- :attr:`Member.premium_since` - :attr:`Member.premium_since`
- :attr:`User.name` - :attr:`User.name`
- :attr:`User.avatar` (:attr:`User.avatar_url` and :meth:`User.avatar_url_as`) - :attr:`User.avatar`
- :attr:`User.discriminator` - :attr:`User.discriminator`
For more information go to the :ref:`member intent documentation <need_members_intent>`. For more information go to the :ref:`member intent documentation <need_members_intent>`.

179
discord/guild.py

@ -94,8 +94,6 @@ class Guild(Hashable):
The timeout to get sent to the AFK channel. The timeout to get sent to the AFK channel.
afk_channel: Optional[:class:`VoiceChannel`] afk_channel: Optional[:class:`VoiceChannel`]
The channel that denotes the AFK channel. ``None`` if it doesn't exist. The channel that denotes the AFK channel. ``None`` if it doesn't exist.
icon: Optional[:class:`str`]
The guild's icon.
id: :class:`int` id: :class:`int`
The guild's ID. The guild's ID.
owner_id: :class:`int` owner_id: :class:`int`
@ -118,8 +116,6 @@ class Guild(Hashable):
The maximum amount of users in a video channel. The maximum amount of users in a video channel.
.. versionadded:: 1.4 .. versionadded:: 1.4
banner: Optional[:class:`str`]
The guild's banner.
description: Optional[:class:`str`] description: Optional[:class:`str`]
The guild's description. The guild's description.
mfa_level: :class:`int` mfa_level: :class:`int`
@ -154,8 +150,6 @@ class Guild(Hashable):
- ``MEMBER_VERIFICATION_GATE_ENABLED``: Guild has Membership Screening enabled. - ``MEMBER_VERIFICATION_GATE_ENABLED``: Guild has Membership Screening enabled.
- ``PREVIEW_ENABLED``: Guild can be viewed before being accepted via Membership Screening. - ``PREVIEW_ENABLED``: Guild can be viewed before being accepted via Membership Screening.
splash: Optional[:class:`str`]
The guild's invite splash.
premium_tier: :class:`int` premium_tier: :class:`int`
The premium tier for this guild. Corresponds to "Nitro Server" in the official UI. The premium tier for this guild. Corresponds to "Nitro Server" in the official UI.
The number goes from 0 to 3 inclusive. The number goes from 0 to 3 inclusive.
@ -164,10 +158,6 @@ class Guild(Hashable):
preferred_locale: Optional[:class:`str`] preferred_locale: Optional[:class:`str`]
The preferred locale for the guild. Used when filtering Server Discovery The preferred locale for the guild. Used when filtering Server Discovery
results to a specific language. results to a specific language.
discovery_splash: :class:`str`
The guild's discovery splash.
.. versionadded:: 1.3
nsfw: :class:`bool` nsfw: :class:`bool`
If the guild is marked as "not safe for work". If the guild is marked as "not safe for work".
@ -175,15 +165,15 @@ class Guild(Hashable):
.. versionadded:: 2.0 .. versionadded:: 2.0
""" """
__slots__ = ('afk_timeout', 'afk_channel', '_members', '_channels', 'icon', __slots__ = ('afk_timeout', 'afk_channel', '_members', '_channels', '_icon',
'name', 'id', 'unavailable', 'banner', 'region', '_state', 'name', 'id', 'unavailable', '_banner', 'region', '_state',
'_roles', '_member_count', '_large', '_roles', '_member_count', '_large',
'owner_id', 'mfa_level', 'emojis', 'features', 'owner_id', 'mfa_level', 'emojis', 'features',
'verification_level', 'explicit_content_filter', 'splash', 'verification_level', 'explicit_content_filter', '_splash',
'_voice_states', '_system_channel_id', 'default_notifications', '_voice_states', '_system_channel_id', 'default_notifications',
'description', 'max_presences', 'max_members', 'max_video_channel_users', 'description', 'max_presences', 'max_members', 'max_video_channel_users',
'premium_tier', 'premium_subscription_count', '_system_channel_flags', 'premium_tier', 'premium_subscription_count', '_system_channel_flags',
'preferred_locale', 'discovery_splash', '_rules_channel_id', 'preferred_locale', '_discovery_splash', '_rules_channel_id',
'_public_updates_channel_id', 'nsfw') '_public_updates_channel_id', 'nsfw')
_PREMIUM_GUILD_LIMITS = { _PREMIUM_GUILD_LIMITS = {
@ -293,8 +283,8 @@ class Guild(Hashable):
self.default_notifications = try_enum(NotificationLevel, guild.get('default_message_notifications')) self.default_notifications = try_enum(NotificationLevel, guild.get('default_message_notifications'))
self.explicit_content_filter = try_enum(ContentFilter, guild.get('explicit_content_filter', 0)) self.explicit_content_filter = try_enum(ContentFilter, guild.get('explicit_content_filter', 0))
self.afk_timeout = guild.get('afk_timeout') self.afk_timeout = guild.get('afk_timeout')
self.icon = guild.get('icon') self._icon = guild.get('icon')
self.banner = guild.get('banner') self._banner = guild.get('banner')
self.unavailable = guild.get('unavailable', False) self.unavailable = guild.get('unavailable', False)
self.id = int(guild['id']) self.id = int(guild['id'])
self._roles = {} self._roles = {}
@ -306,7 +296,7 @@ class Guild(Hashable):
self.mfa_level = guild.get('mfa_level') self.mfa_level = guild.get('mfa_level')
self.emojis = tuple(map(lambda d: state.store_emoji(self, d), guild.get('emojis', []))) self.emojis = tuple(map(lambda d: state.store_emoji(self, d), guild.get('emojis', [])))
self.features = guild.get('features', []) self.features = guild.get('features', [])
self.splash = guild.get('splash') self._splash = guild.get('splash')
self._system_channel_id = utils._get_as_snowflake(guild, 'system_channel_id') self._system_channel_id = utils._get_as_snowflake(guild, 'system_channel_id')
self.description = guild.get('description') self.description = guild.get('description')
self.max_presences = guild.get('max_presences') self.max_presences = guild.get('max_presences')
@ -316,7 +306,7 @@ class Guild(Hashable):
self.premium_subscription_count = guild.get('premium_subscription_count') or 0 self.premium_subscription_count = guild.get('premium_subscription_count') or 0
self._system_channel_flags = guild.get('system_channel_flags', 0) self._system_channel_flags = guild.get('system_channel_flags', 0)
self.preferred_locale = guild.get('preferred_locale') self.preferred_locale = guild.get('preferred_locale')
self.discovery_splash = guild.get('discovery_splash') self._discovery_splash = guild.get('discovery_splash')
self._rules_channel_id = utils._get_as_snowflake(guild, 'rules_channel_id') self._rules_channel_id = utils._get_as_snowflake(guild, 'rules_channel_id')
self._public_updates_channel_id = utils._get_as_snowflake(guild, 'public_updates_channel_id') self._public_updates_channel_id = utils._get_as_snowflake(guild, 'public_updates_channel_id')
self.nsfw = guild.get('nsfw', False) self.nsfw = guild.get('nsfw', False)
@ -621,139 +611,32 @@ class Guild(Hashable):
return self.get_member(self.owner_id) return self.get_member(self.owner_id)
@property @property
def icon_url(self): def icon(self):
""":class:`Asset`: Returns the guild's icon asset.""" """Optional[:class:`Asset`]: Returns the guild's icon asset, if available."""
return self.icon_url_as() if self._icon is None:
return None
def is_icon_animated(self): return Asset._from_guild_icon(self._state, self.id, self._icon)
""":class:`bool`: Returns True if the guild has an animated icon."""
return bool(self.icon and self.icon.startswith('a_'))
def icon_url_as(self, *, format=None, static_format='webp', size=1024):
"""Returns an :class:`Asset` for the guild's icon.
The format must be one of 'webp', 'jpeg', 'jpg', 'png' or 'gif', and
'gif' is only valid for animated avatars. The size must be a power of 2
between 16 and 4096.
Parameters
-----------
format: Optional[:class:`str`]
The format to attempt to convert the icon to.
If the format is ``None``, then it is automatically
detected into either 'gif' or static_format depending on the
icon being animated or not.
static_format: Optional[:class:`str`]
Format to attempt to convert only non-animated icons to.
size: :class:`int`
The size of the image to display.
Raises
------
InvalidArgument
Bad image format passed to ``format`` or invalid ``size``.
Returns
--------
:class:`Asset`
The resulting CDN asset.
"""
return Asset._from_guild_icon(self._state, self, format=format, static_format=static_format, size=size)
@property @property
def banner_url(self): def banner(self):
""":class:`Asset`: Returns the guild's banner asset.""" """Optional[:class:`Asset`]: Returns the guild's banner asset, if available."""
return self.banner_url_as() if self._banner is None:
return None
def banner_url_as(self, *, format='webp', size=2048): return Asset._from_guild_image(self._state, self.id, self._banner, path='banners')
"""Returns an :class:`Asset` for the guild's banner.
The format must be one of 'webp', 'jpeg', or 'png'. The
size must be a power of 2 between 16 and 4096.
Parameters
-----------
format: :class:`str`
The format to attempt to convert the banner to.
size: :class:`int`
The size of the image to display.
Raises
------
InvalidArgument
Bad image format passed to ``format`` or invalid ``size``.
Returns
--------
:class:`Asset`
The resulting CDN asset.
"""
return Asset._from_guild_image(self._state, self.id, self.banner, 'banners', format=format, size=size)
@property @property
def splash_url(self): def splash(self):
""":class:`Asset`: Returns the guild's invite splash asset.""" """Optional[:class:`Asset`]: Returns the guild's invite splash asset, if available."""
return self.splash_url_as() if self._splash is None:
return None
def splash_url_as(self, *, format='webp', size=2048): return Asset._from_guild_image(self._state, self.id, self._splash, path='splashes')
"""Returns an :class:`Asset` for the guild's invite splash.
The format must be one of 'webp', 'jpeg', 'jpg', or 'png'. The
size must be a power of 2 between 16 and 4096.
Parameters
-----------
format: :class:`str`
The format to attempt to convert the splash to.
size: :class:`int`
The size of the image to display.
Raises
------
InvalidArgument
Bad image format passed to ``format`` or invalid ``size``.
Returns
--------
:class:`Asset`
The resulting CDN asset.
"""
return Asset._from_guild_image(self._state, self.id, self.splash, 'splashes', format=format, size=size)
@property @property
def discovery_splash_url(self): def discovery_splash(self):
""":class:`Asset`: Returns the guild's discovery splash asset. """Optional[:class:`Asset`]: Returns the guild's discovery splash asset, if available."""
if self._discovery_splash is None:
.. versionadded:: 1.3 return None
""" return Asset._from_guild_image(self._state, self.id, self._discovery_splash, path='discovery-splashes')
return self.discovery_splash_url_as()
def discovery_splash_url_as(self, *, format='webp', size=2048):
"""Returns an :class:`Asset` for the guild's discovery splash.
The format must be one of 'webp', 'jpeg', 'jpg', or 'png'. The
size must be a power of 2 between 16 and 4096.
.. versionadded:: 1.3
Parameters
-----------
format: :class:`str`
The format to attempt to convert the splash to.
size: :class:`int`
The size of the image to display.
Raises
------
InvalidArgument
Bad image format passed to ``format`` or invalid ``size``.
Returns
--------
:class:`Asset`
The resulting CDN asset.
"""
return Asset._from_guild_image(self._state, self.id, self.discovery_splash, 'discovery-splashes', format=format, size=size)
@property @property
def member_count(self): def member_count(self):
@ -1185,7 +1068,7 @@ class Guild(Hashable):
try: try:
icon_bytes = fields['icon'] icon_bytes = fields['icon']
except KeyError: except KeyError:
icon = self.icon icon = self._icon
else: else:
if icon_bytes is not None: if icon_bytes is not None:
icon = utils._bytes_to_base64_data(icon_bytes) icon = utils._bytes_to_base64_data(icon_bytes)
@ -1195,7 +1078,7 @@ class Guild(Hashable):
try: try:
banner_bytes = fields['banner'] banner_bytes = fields['banner']
except KeyError: except KeyError:
banner = self.banner banner = self._banner
else: else:
if banner_bytes is not None: if banner_bytes is not None:
banner = utils._bytes_to_base64_data(banner_bytes) banner = utils._bytes_to_base64_data(banner_bytes)
@ -1212,7 +1095,7 @@ class Guild(Hashable):
try: try:
splash_bytes = fields['splash'] splash_bytes = fields['splash']
except KeyError: except KeyError:
splash = self.splash splash = self._splash
else: else:
if splash_bytes is not None: if splash_bytes is not None:
splash = utils._bytes_to_base64_data(splash_bytes) splash = utils._bytes_to_base64_data(splash_bytes)

75
discord/invite.py

@ -140,26 +140,20 @@ class PartialInviteGuild:
The partial guild's verification level. The partial guild's verification level.
features: List[:class:`str`] features: List[:class:`str`]
A list of features the guild has. See :attr:`Guild.features` for more information. A list of features the guild has. See :attr:`Guild.features` for more information.
icon: Optional[:class:`str`]
The partial guild's icon.
banner: Optional[:class:`str`]
The partial guild's banner.
splash: Optional[:class:`str`]
The partial guild's invite splash.
description: Optional[:class:`str`] description: Optional[:class:`str`]
The partial guild's description. The partial guild's description.
""" """
__slots__ = ('_state', 'features', 'icon', 'banner', 'id', 'name', 'splash', 'verification_level', 'description') __slots__ = ('_state', 'features', '_icon', '_banner', 'id', 'name', '_splash', 'verification_level', 'description')
def __init__(self, state, data: InviteGuildPayload, id: int): def __init__(self, state, data: InviteGuildPayload, id: int):
self._state = state self._state = state
self.id = id self.id = id
self.name = data['name'] self.name = data['name']
self.features = data.get('features', []) self.features = data.get('features', [])
self.icon = data.get('icon') self._icon = data.get('icon')
self.banner = data.get('banner') self._banner = data.get('banner')
self.splash = data.get('splash') self._splash = data.get('splash')
self.verification_level = try_enum(VerificationLevel, data.get('verification_level')) self.verification_level = try_enum(VerificationLevel, data.get('verification_level'))
self.description = data.get('description') self.description = data.get('description')
@ -178,56 +172,25 @@ class PartialInviteGuild:
return snowflake_time(self.id) return snowflake_time(self.id)
@property @property
def icon_url(self) -> Asset: def icon_url(self) -> Optional[Asset]:
""":class:`Asset`: Returns the guild's icon asset.""" """Optional[:class:`Asset`]: Returns the guild's icon asset, if available."""
return self.icon_url_as() if self._icon is None:
return None
def is_icon_animated(self) -> bool: return Asset._from_guild_icon(self._state, self.id, self._icon)
""":class:`bool`: Returns ``True`` if the guild has an animated icon.
.. versionadded:: 1.4
"""
return bool(self.icon and self.icon.startswith('a_'))
def icon_url_as(self, *, format=None, static_format='webp', size=1024) -> Asset:
"""The same operation as :meth:`Guild.icon_url_as`.
Returns
--------
:class:`Asset`
The resulting CDN asset.
"""
return Asset._from_guild_icon(self._state, self, format=format, static_format=static_format, size=size)
@property @property
def banner_url(self) -> Asset: def banner(self) -> Optional[Asset]:
""":class:`Asset`: Returns the guild's banner asset.""" """Optional[:class:`Asset`]: Returns the guild's banner asset, if available."""
return self.banner_url_as() if self._banner is None:
return None
def banner_url_as(self, *, format='webp', size=2048) -> Asset: return Asset._from_guild_image(self._state, self.id, self._banner, path='banners')
"""The same operation as :meth:`Guild.banner_url_as`.
Returns
--------
:class:`Asset`
The resulting CDN asset.
"""
return Asset._from_guild_image(self._state, self.id, self.banner, 'banners', format=format, size=size)
@property @property
def splash_url(self) -> Asset: def splash(self) -> Optional[Asset]:
""":class:`Asset`: Returns the guild's invite splash asset.""" """Optional[:class:`Asset`]: Returns the guild's invite splash asset, if available."""
return self.splash_url_as() if self._splash is None:
return None
def splash_url_as(self, *, format='webp', size=2048) -> Asset: return Asset._from_guild_image(self._state, self.id, self._splash, path='splashes')
"""The same operation as :meth:`Guild.splash_url_as`.
Returns
--------
:class:`Asset`
The resulting CDN asset.
"""
return Asset._from_guild_image(self._state, self.id, self.splash, 'splashes', format=format, size=size)
class Invite(Hashable): class Invite(Hashable):

6
discord/member.py

@ -325,12 +325,12 @@ class Member(discord.abc.Messageable, _BaseUser):
def _update_inner_user(self, user): def _update_inner_user(self, user):
u = self._user u = self._user
original = (u.name, u.avatar, u.discriminator, u._public_flags) original = (u.name, u._avatar, u.discriminator, u._public_flags)
# These keys seem to always be available # These keys seem to always be available
modified = (user['username'], user['avatar'], user['discriminator'], user.get('public_flags', 0)) modified = (user['username'], user['avatar'], user['discriminator'], user.get('public_flags', 0))
if original != modified: if original != modified:
to_return = User._copy(self._user) to_return = User._copy(self._user)
u.name, u.avatar, u.discriminator, u._public_flags = modified u.name, u._avatar, u.discriminator, u._public_flags = modified
# Signal to dispatch on_user_update # Signal to dispatch on_user_update
return to_return, u return to_return, u
@ -442,7 +442,7 @@ class Member(discord.abc.Messageable, _BaseUser):
.. note:: .. note::
Due to a Discord API limitation, this may be ``None`` if Due to a Discord API limitation, this may be ``None`` if
the user is listening to a song on Spotify with a title longer the user is listening to a song on Spotify with a title longer
than 128 characters. See :issue:`1738` for more information. than 128 characters. See :issue:`1738` for more information.

4
discord/message.py

@ -172,11 +172,11 @@ class Attachment(Hashable):
The number of bytes written. The number of bytes written.
""" """
data = await self.read(use_cached=use_cached) data = await self.read(use_cached=use_cached)
if isinstance(fp, io.IOBase) and fp.writable(): if isinstance(fp, io.RawIOBase):
written = fp.write(data) written = fp.write(data)
if seek_begin: if seek_begin:
fp.seek(0) fp.seek(0)
return written return written or 0
else: else:
with open(fp, 'wb') as f: with open(fp, 'wb') as f:
return f.write(data) return f.write(data)

96
discord/partial_emoji.py

@ -22,7 +22,10 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE. DEALINGS IN THE SOFTWARE.
""" """
import io
from .asset import Asset from .asset import Asset
from .errors import DiscordException, InvalidArgument
from . import utils from . import utils
__all__ = ( __all__ = (
@ -149,44 +152,83 @@ class PartialEmoji(_EmojiTag):
return utils.snowflake_time(self.id) return utils.snowflake_time(self.id)
@property @property
def url(self): def url(self) -> str:
""":class:`Asset`: Returns the asset of the emoji, if it is custom. """:class:`str`: Returns the URL of the emoji, if it is custom.
This is equivalent to calling :meth:`url_as` with If this isn't a custom emoji then an empty string is returned
the default parameters (i.e. png/gif detection).
""" """
return self.url_as(format=None) if self.is_unicode_emoji():
return ''
def url_as(self, *, format=None, static_format="png"): fmt = 'gif' if self.animated else 'png'
"""Returns an :class:`Asset` for the emoji's url, if it is custom. return f'{Asset.BASE}/emojis/{self.id}.{fmt}'
The format must be one of 'webp', 'jpeg', 'jpg', 'png' or 'gif'. async def read(self):
'gif' is only valid for animated emojis. """|coro|
.. versionadded:: 1.7 Retrieves the content of this emoji as a :class:`bytes` object.
Parameters .. versionadded:: 2.0
-----------
format: Optional[:class:`str`]
The format to attempt to convert the emojis to.
If the format is ``None``, then it is automatically
detected as either 'gif' or static_format, depending on whether the
emoji is animated or not.
static_format: Optional[:class:`str`]
Format to attempt to convert only non-animated emoji's to.
Defaults to 'png'
Raises Raises
------- ------
DiscordException
There was no internal connection state.
InvalidArgument InvalidArgument
Bad image format passed to ``format`` or ``static_format``. The emoji isn't custom.
HTTPException
Downloading the emoji failed.
NotFound
The emoji was deleted.
Returns Returns
-------- -------
:class:`Asset` :class:`bytes`
The resulting CDN asset. The content of the emoji.
""" """
if self._state is None:
raise DiscordException('Invalid state (no ConnectionState provided)')
if self.is_unicode_emoji(): if self.is_unicode_emoji():
return Asset(self._state) raise InvalidArgument('PartialEmoji is not a custom emoji')
return await self._state.http.get_from_cdn(self.url)
async def save(self, fp, *, seek_begin=True):
"""|coro|
Saves this emoji into a file-like object.
.. versionadded:: 2.0
Parameters
----------
fp: Union[BinaryIO, :class:`os.PathLike`]
Same as in :meth:`Attachment.save`.
seek_begin: :class:`bool`
Same as in :meth:`Attachment.save`.
Raises
------
DiscordException
There was no internal connection state.
HTTPException
Downloading the emoji failed.
NotFound
The emoji was deleted.
Returns
--------
:class:`int`
The number of bytes written.
"""
return Asset._from_emoji(self._state, self, format=format, static_format=static_format) data = await self.read()
if isinstance(fp, io.IOBase) and fp.writable():
written = fp.write(data)
if seek_begin:
fp.seek(0)
return written
else:
with open(fp, 'wb') as f:
return f.write(data)

35
discord/sticker.py

@ -62,12 +62,10 @@ class Sticker(Hashable):
The id of the sticker's pack. The id of the sticker's pack.
format: :class:`StickerType` format: :class:`StickerType`
The format for the sticker's image. The format for the sticker's image.
image: :class:`str`
The sticker's image.
tags: List[:class:`str`] tags: List[:class:`str`]
A list of tags for the sticker. A list of tags for the sticker.
""" """
__slots__ = ('_state', 'id', 'name', 'description', 'pack_id', 'format', 'image', 'tags') __slots__ = ('_state', 'id', 'name', 'description', 'pack_id', 'format', '_image', 'tags')
def __init__(self, *, state, data): def __init__(self, *, state, data):
self._state = state self._state = state
@ -76,7 +74,7 @@ class Sticker(Hashable):
self.description = data['description'] self.description = data['description']
self.pack_id = int(data['pack_id']) self.pack_id = int(data['pack_id'])
self.format = try_enum(StickerType, data['format_type']) self.format = try_enum(StickerType, data['format_type'])
self.image = data['asset'] self._image = data['asset']
try: try:
self.tags = [tag.strip() for tag in data['tags'].split(',')] self.tags = [tag.strip() for tag in data['tags'].split(',')]
@ -95,7 +93,7 @@ class Sticker(Hashable):
return snowflake_time(self.id) return snowflake_time(self.id)
@property @property
def image_url(self): def image(self):
"""Returns an :class:`Asset` for the sticker's image. """Returns an :class:`Asset` for the sticker's image.
.. note:: .. note::
@ -106,32 +104,7 @@ class Sticker(Hashable):
Optional[:class:`Asset`] Optional[:class:`Asset`]
The resulting CDN asset. The resulting CDN asset.
""" """
return self.image_url_as()
def image_url_as(self, *, size=1024):
"""Optionally returns an :class:`Asset` for the sticker's image.
The size must be a power of 2 between 16 and 4096.
.. note::
This will return ``None`` if the format is ``StickerType.lottie``.
Parameters
-----------
size: :class:`int`
The size of the image to display.
Raises
------
InvalidArgument
Invalid ``size``.
Returns
-------
Optional[:class:`Asset`]
The resulting CDN asset or ``None``.
"""
if self.format is StickerType.lottie: if self.format is StickerType.lottie:
return None return None
return Asset._from_sticker_url(self._state, self, size=size) return Asset._from_sticker_url(self._state, self.id, self._image)

47
discord/team.py

@ -42,8 +42,6 @@ class Team:
The team ID. The team ID.
name: :class:`str` name: :class:`str`
The team name The team name
icon: Optional[:class:`str`]
The icon hash, if it exists.
owner_id: :class:`int` owner_id: :class:`int`
The team's owner ID. The team's owner ID.
members: List[:class:`TeamMember`] members: List[:class:`TeamMember`]
@ -52,14 +50,14 @@ class Team:
.. versionadded:: 1.3 .. versionadded:: 1.3
""" """
__slots__ = ('_state', 'id', 'name', 'icon', 'owner_id', 'members') __slots__ = ('_state', 'id', 'name', '_icon', 'owner_id', 'members')
def __init__(self, state, data): def __init__(self, state, data):
self._state = state self._state = state
self.id = utils._get_as_snowflake(data, 'id') self.id = int(data['id'])
self.name = data['name'] self.name = data['name']
self.icon = data['icon'] self._icon = data['icon']
self.owner_id = utils._get_as_snowflake(data, 'owner_user_id') self.owner_id = utils._get_as_snowflake(data, 'owner_user_id')
self.members = [TeamMember(self, self._state, member) for member in data['members']] self.members = [TeamMember(self, self._state, member) for member in data['members']]
@ -67,40 +65,11 @@ class Team:
return f'<{self.__class__.__name__} id={self.id} name={self.name}>' return f'<{self.__class__.__name__} id={self.id} name={self.name}>'
@property @property
def icon_url(self): def icon(self):
""":class:`.Asset`: Retrieves the team's icon asset. """Optional[:class:`.Asset`]: Retrieves the team's icon asset, if any."""
if self._icon is None:
This is equivalent to calling :meth:`icon_url_as` with return None
the default parameters ('webp' format and a size of 1024). return Asset._from_icon(self._state, self.id, self._icon, path='team')
"""
return self.icon_url_as()
def icon_url_as(self, *, format='webp', size=1024):
"""Returns an :class:`Asset` for the icon the team has.
The format must be one of 'webp', 'jpeg', 'jpg' or 'png'.
The size must be a power of 2 between 16 and 4096.
.. versionadded:: 2.0
Parameters
-----------
format: :class:`str`
The format to attempt to convert the icon to. Defaults to 'webp'.
size: :class:`int`
The size of the image to display.
Raises
------
InvalidArgument
Bad image format passed to ``format`` or invalid ``size``.
Returns
--------
:class:`Asset`
The resulting CDN asset.
"""
return Asset._from_icon(self._state, self, 'team', format=format, size=size)
@property @property
def owner(self): def owner(self):

68
discord/user.py

@ -38,7 +38,7 @@ _BaseUser = discord.abc.User
class BaseUser(_BaseUser): class BaseUser(_BaseUser):
__slots__ = ('name', 'id', 'discriminator', 'avatar', 'bot', 'system', '_public_flags', '_state') __slots__ = ('name', 'id', 'discriminator', '_avatar', 'bot', 'system', '_public_flags', '_state')
def __init__(self, *, state, data): def __init__(self, *, state, data):
self._state = state self._state = state
@ -60,7 +60,7 @@ class BaseUser(_BaseUser):
self.name = data['username'] self.name = data['username']
self.id = int(data['id']) self.id = int(data['id'])
self.discriminator = data['discriminator'] self.discriminator = data['discriminator']
self.avatar = data['avatar'] self._avatar = data['avatar']
self._public_flags = data.get('public_flags', 0) self._public_flags = data.get('public_flags', 0)
self.bot = data.get('bot', False) self.bot = data.get('bot', False)
self.system = data.get('system', False) self.system = data.get('system', False)
@ -72,7 +72,7 @@ class BaseUser(_BaseUser):
self.name = user.name self.name = user.name
self.id = user.id self.id = user.id
self.discriminator = user.discriminator self.discriminator = user.discriminator
self.avatar = user.avatar self._avatar = user._avatar
self.bot = user.bot self.bot = user.bot
self._state = user._state self._state = user._state
self._public_flags = user._public_flags self._public_flags = user._public_flags
@ -83,7 +83,7 @@ class BaseUser(_BaseUser):
return { return {
'username': self.name, 'username': self.name,
'id': self.id, 'id': self.id,
'avatar': self.avatar, 'avatar': self._avatar,
'discriminator': self.discriminator, 'discriminator': self.discriminator,
'bot': self.bot, 'bot': self.bot,
} }
@ -94,66 +94,20 @@ class BaseUser(_BaseUser):
return PublicUserFlags._from_value(self._public_flags) return PublicUserFlags._from_value(self._public_flags)
@property @property
def avatar_url(self): def avatar(self):
""":class:`Asset`: Returns an :class:`Asset` for the avatar the user has. """:class:`Asset`: Returns an :class:`Asset` for the avatar the user has.
If the user does not have a traditional avatar, an asset for If the user does not have a traditional avatar, an asset for
the default avatar is returned instead. the default avatar is returned instead.
This is equivalent to calling :meth:`avatar_url_as` with
the default parameters (i.e. webp/gif detection and a size of 1024).
"""
return self.avatar_url_as(format=None, size=1024)
def is_avatar_animated(self):
""":class:`bool`: Indicates if the user has an animated avatar."""
return bool(self.avatar and self.avatar.startswith('a_'))
def avatar_url_as(self, *, format=None, static_format='webp', size=1024):
"""Returns an :class:`Asset` for the avatar the user has.
If the user does not have a traditional avatar, an asset for
the default avatar is returned instead.
The format must be one of 'webp', 'jpeg', 'jpg', 'png' or 'gif', and
'gif' is only valid for animated avatars. The size must be a power of 2
between 16 and 4096.
Parameters
-----------
format: Optional[:class:`str`]
The format to attempt to convert the avatar to.
If the format is ``None``, then it is automatically
detected into either 'gif' or static_format depending on the
avatar being animated or not.
static_format: Optional[:class:`str`]
Format to attempt to convert only non-animated avatars to.
Defaults to 'webp'
size: :class:`int`
The size of the image to display.
Raises
------
InvalidArgument
Bad image format passed to ``format`` or ``static_format``, or
invalid ``size``.
Returns
--------
:class:`Asset`
The resulting CDN asset.
""" """
return Asset._from_avatar(self._state, self, format=format, static_format=static_format, size=size) if self._avatar is None:
return Asset._from_default_avatar(self._state, int(self.discriminator) % len(DefaultAvatar))
else:
return Asset._from_avatar(self._state, self.id, self._avatar)
@property @property
def default_avatar(self): def default_avatar(self):
""":class:`DefaultAvatar`: Returns the default avatar for a given user. This is calculated by the user's discriminator.""" """:class:`Asset`: Returns the default avatar for a given user. This is calculated by the user's discriminator."""
return try_enum(DefaultAvatar, int(self.discriminator) % len(DefaultAvatar)) return Asset._from_default_avatar(self._state, int(self.discriminator) % len(DefaultAvatar))
@property
def default_avatar_url(self):
""":class:`Asset`: Returns a URL for a user's default avatar."""
return Asset(self._state, f'/embed/avatars/{self.default_avatar.value}.png')
@property @property
def colour(self): def colour(self):

104
discord/webhook/async_.py

@ -475,56 +475,23 @@ class PartialWebhookGuild(Hashable):
The partial guild's icon The partial guild's icon
""" """
__slots__ = ('id', 'name', 'icon', '_state') __slots__ = ('id', 'name', '_icon', '_state')
def __init__(self, *, data, state): def __init__(self, *, data, state):
self._state = state self._state = state
self.id = int(data['id']) self.id = int(data['id'])
self.name = data['name'] self.name = data['name']
self.icon = data['icon'] self._icon = data['icon']
def __repr__(self): def __repr__(self):
return f'<PartialWebhookGuild name={self.name!r} id={self.id}>' return f'<PartialWebhookGuild name={self.name!r} id={self.id}>'
@property @property
def icon_url(self) -> Asset: def icon_url(self) -> Optional[Asset]:
""":class:`Asset`: Returns the guild's icon asset.""" """Optional[:class:`Asset`]: Returns the guild's icon asset, if available."""
return self.icon_url_as() if self._icon is None:
return None
def is_icon_animated(self) -> bool: return Asset._from_guild_icon(self._state, self.id, self._icon)
""":class:`bool`: Returns True if the guild has an animated icon."""
return bool(self.icon and self.icon.startswith('a_'))
def icon_url_as(self, *, format=None, static_format='webp', size=1024):
"""Returns an :class:`Asset` for the guild's icon.
The format must be one of 'webp', 'jpeg', 'jpg', 'png' or 'gif', and
'gif' is only valid for animated avatars. The size must be a power of 2
between 16 and 4096.
Parameters
-----------
format: Optional[:class:`str`]
The format to attempt to convert the icon to.
If the format is ``None``, then it is automatically
detected into either 'gif' or static_format depending on the
icon being animated or not.
static_format: Optional[:class:`str`]
Format to attempt to convert only non-animated icons to.
size: :class:`int`
The size of the image to display.
Raises
------
InvalidArgument
Bad image format passed to ``format`` or invalid ``size``.
Returns
--------
:class:`Asset`
The resulting CDN asset.
"""
return Asset._from_guild_icon(self._state, self, format=format, static_format=static_format, size=size)
class _FriendlyHttpAttributeErrorHelper: class _FriendlyHttpAttributeErrorHelper:
@ -684,7 +651,7 @@ class BaseWebhook(Hashable):
'auth_token', 'auth_token',
'user', 'user',
'name', 'name',
'avatar', '_avatar',
'source_channel', 'source_channel',
'source_guild', 'source_guild',
'_state', '_state',
@ -701,7 +668,7 @@ class BaseWebhook(Hashable):
self.channel_id = utils._get_as_snowflake(data, 'channel_id') self.channel_id = utils._get_as_snowflake(data, 'channel_id')
self.guild_id = utils._get_as_snowflake(data, 'guild_id') self.guild_id = utils._get_as_snowflake(data, 'guild_id')
self.name = data.get('name') self.name = data.get('name')
self.avatar = data.get('avatar') self._avatar = data.get('avatar')
self.token = data.get('token') self.token = data.get('token')
user = data.get('user') user = data.get('user')
@ -755,59 +722,16 @@ class BaseWebhook(Hashable):
return utils.snowflake_time(self.id) return utils.snowflake_time(self.id)
@property @property
def avatar_url(self) -> Asset: def avatar(self) -> Asset:
""":class:`Asset`: Returns an :class:`Asset` for the avatar the webhook has. """:class:`Asset`: Returns an :class:`Asset` for the avatar the webhook has.
If the webhook does not have a traditional avatar, an asset for If the webhook does not have a traditional avatar, an asset for
the default avatar is returned instead. the default avatar is returned instead.
This is equivalent to calling :meth:`avatar_url_as` with the
default parameters.
"""
return self.avatar_url_as()
def avatar_url_as(self, *, format: Optional[Literal['png', 'jpg', 'jpeg']] = None, size: int = 1024) -> Asset:
"""Returns an :class:`Asset` for the avatar the webhook has.
If the webhook does not have a traditional avatar, an asset for
the default avatar is returned instead.
The format must be one of 'jpeg', 'jpg', or 'png'.
The size must be a power of 2 between 16 and 1024.
Parameters
-----------
format: Optional[:class:`str`]
The format to attempt to convert the avatar to.
If the format is ``None``, then it is equivalent to png.
size: :class:`int`
The size of the image to display.
Raises
------
InvalidArgument
Bad image format passed to ``format`` or invalid ``size``.
Returns
--------
:class:`Asset`
The resulting CDN asset.
""" """
if self.avatar is None: if self._avatar is None:
# Default is always blurple apparently # Default is always blurple apparently
return Asset(self._state, '/embed/avatars/0.png') return Asset._from_default_avatar(self._state, 0)
return Asset._from_avatar(self._state, self.id, self._avatar)
if not utils.valid_icon_size(size):
raise InvalidArgument("size must be a power of 2 between 16 and 1024")
format = format or 'png'
if format not in ('png', 'jpg', 'jpeg'):
raise InvalidArgument("format must be one of 'png', 'jpg', or 'jpeg'.")
url = f'/avatars/{self.id}/{self.avatar}.{format}?size={size}'
return Asset(self._state, url)
class Webhook(BaseWebhook): class Webhook(BaseWebhook):
"""Represents an asynchronous Discord webhook. """Represents an asynchronous Discord webhook.
@ -980,7 +904,7 @@ class Webhook(BaseWebhook):
'name': name, 'name': name,
'channel_id': channel.id, 'channel_id': channel.id,
'guild_id': channel.guild.id, 'guild_id': channel.guild.id,
'user': {'username': user.name, 'discriminator': user.discriminator, 'id': user.id, 'avatar': user.avatar}, 'user': {'username': user.name, 'discriminator': user.discriminator, 'id': user.id, 'avatar': user._avatar},
} }
state = channel._state state = channel._state

Loading…
Cancel
Save