aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRapptz <[email protected]>2021-04-16 11:21:13 -0400
committerRapptz <[email protected]>2021-04-16 11:27:23 -0400
commit9eaf1e85e4e987b5f874a7ba4c3ed13de10fd154 (patch)
tree83a60b0aaff3c1b63868631418cb7583adda054d
parentAdd `fetch_message` for webhooks (diff)
downloaddiscord.py-9eaf1e85e4e987b5f874a7ba4c3ed13de10fd154.tar.xz
discord.py-9eaf1e85e4e987b5f874a7ba4c3ed13de10fd154.zip
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.
-rw-r--r--discord/appinfo.py98
-rw-r--r--discord/asset.py339
-rw-r--r--discord/channel.py57
-rw-r--r--discord/emoji.py104
-rw-r--r--discord/flags.py2
-rw-r--r--discord/guild.py179
-rw-r--r--discord/invite.py75
-rw-r--r--discord/member.py6
-rw-r--r--discord/message.py4
-rw-r--r--discord/partial_emoji.py96
-rw-r--r--discord/sticker.py35
-rw-r--r--discord/team.py47
-rw-r--r--discord/user.py68
-rw-r--r--discord/webhook/async_.py104
14 files changed, 488 insertions, 726 deletions
diff --git a/discord/appinfo.py b/discord/appinfo.py
index 6f1754ff..db60502f 100644
--- a/discord/appinfo.py
+++ b/discord/appinfo.py
@@ -49,8 +49,6 @@ class AppInfo:
.. versionadded:: 1.3
- icon: Optional[:class:`str`]
- The icon hash, if it exists.
description: Optional[:class:`str`]
The application description.
bot_public: :class:`bool`
@@ -89,12 +87,6 @@ class AppInfo:
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
"""
__slots__ = (
@@ -106,14 +98,14 @@ class AppInfo:
'bot_public',
'bot_require_code_grant',
'owner',
- 'icon',
+ '_icon',
'summary',
'verify_key',
'team',
'guild_id',
'primary_sku_id',
'slug',
- 'cover_image',
+ '_cover_image',
)
def __init__(self, state, data):
@@ -122,7 +114,7 @@ class AppInfo:
self.id = int(data['id'])
self.name = data['name']
self.description = data['description']
- self.icon = data['icon']
+ self._icon = data['icon']
self.rpc_origins = data['rpc_origins']
self.bot_public = data['bot_public']
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.slug = data.get('slug')
- self.cover_image = data.get('cover_image')
+ self._cover_image = data.get('cover_image')
def __repr__(self):
return (
@@ -148,81 +140,21 @@ class AppInfo:
)
@property
- def icon_url(self):
- """:class:`.Asset`: Retrieves the application's icon asset.
-
- This is equivalent to calling :meth:`icon_url_as` with
- the default parameters ('webp' format and a size of 1024).
-
- .. 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)
+ def icon(self):
+ """Optional[:class:`.Asset`]: Retrieves the application's icon asset, if any."""
+ if self._icon is None:
+ return None
+ return Asset._from_icon(self._state, self.id, self._icon, path='app')
@property
- def cover_image_url(self):
- """:class:`.Asset`: Retrieves the cover image on a store embed.
-
- 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``.
+ def cover_image(self):
+ """Optional[:class:`.Asset`]: Retrieves the cover image on a store embed, if any.
- Returns
- --------
- :class:`Asset`
- The resulting CDN asset.
+ This is only available if the application is a game sold on Discord.
"""
- 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
def guild(self):
diff --git a/discord/asset.py b/discord/asset.py
index 13f9336f..bf7cd6ca 100644
--- a/discord/asset.py
+++ b/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.
"""
+from __future__ import annotations
+
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 InvalidArgument
from . import utils
+import yarl
+
__all__ = (
'Asset',
)
if TYPE_CHECKING:
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_AVATAR_FORMATS = VALID_STATIC_FORMATS | {"gif"}
+VALID_ASSET_FORMATS = VALID_STATIC_FORMATS | {"gif"}
+
class Asset:
"""Represents a CDN asset on Discord.
@@ -52,10 +58,6 @@ class Asset:
Returns the length of the CDN asset's URL.
- .. describe:: bool(x)
-
- Checks if the Asset has a URL.
-
.. describe:: x == y
Checks if the asset is equal to another asset.
@@ -68,96 +70,88 @@ class Asset:
Returns the hash of the asset.
"""
- __slots__ = ('_state', '_url')
+
+ __slots__: Tuple[str, ...] = (
+ '_state',
+ '_url',
+ '_animated',
+ '_key',
+ )
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._url = url
+ self._animated = animated
+ self._key = key
@classmethod
- def _from_avatar(cls, state, user, *, format=None, static_format='webp', size=1024):
- if not utils.valid_icon_size(size):
- raise InvalidArgument("size must be a power of 2 between 16 and 4096")
- if format is not None and format not in VALID_AVATAR_FORMATS:
- raise InvalidArgument(f"format must be None or one of {VALID_AVATAR_FORMATS}")
- if format == "gif" and not user.is_avatar_animated():
- 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}')
+ def _from_default_avatar(cls, state, index: int) -> Asset:
+ return cls(
+ state,
+ url=f'{cls.BASE}/embed/avatars/{index}.png',
+ key=str(index),
+ animated=False,
+ )
@classmethod
- def _from_icon(cls, state, object, path, *, format='webp', size=1024):
- if object.icon is None:
- return cls(state)
-
- if not utils.valid_icon_size(size):
- raise InvalidArgument("size must be a power of 2 between 16 and 4096")
- if format not in VALID_STATIC_FORMATS:
- raise InvalidArgument(f"format must be None or one of {VALID_STATIC_FORMATS}")
-
- url = f'/{path}-icons/{object.id}/{object.icon}.{format}?size={size}'
- return cls(state, url)
+ def _from_avatar(cls, state, user_id: int, avatar: str) -> Asset:
+ animated = avatar.startswith('a_')
+ format = 'gif' if animated else 'png'
+ return cls(
+ state,
+ url=f'{cls.BASE}/avatars/{user_id}/{avatar}.{format}?size=1024',
+ key=avatar,
+ animated=animated,
+ )
@classmethod
- def _from_cover_image(cls, state, obj, *, format='webp', size=1024):
- if obj.cover_image is None:
- return cls(state)
-
- if not utils.valid_icon_size(size):
- raise InvalidArgument("size must be a power of 2 between 16 and 4096")
- 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)
+ def _from_icon(cls, state, object_id: int, icon_hash: str, path: str) -> Asset:
+ return cls(
+ state,
+ url=f'{cls.BASE}/{path}-icons/{object_id}/{icon_hash}.png?size=1024',
+ key=icon_hash,
+ animated=False,
+ )
@classmethod
- def _from_guild_image(cls, state, id, hash, key, *, format='webp', size=1024):
- if not utils.valid_icon_size(size):
- raise InvalidArgument("size must be a power of 2 between 16 and 4096")
- if format not in VALID_STATIC_FORMATS:
- raise InvalidArgument(f"format must be one of {VALID_STATIC_FORMATS}")
-
- if hash is None:
- return cls(state)
-
- return cls(state, f'/{key}/{id}/{hash}.{format}?size={size}')
+ def _from_cover_image(cls, state, object_id: int, cover_image_hash: str) -> Asset:
+ return cls(
+ state,
+ url=f'{cls.BASE}/app-assets/{object_id}/store/{cover_image_hash}.png?size=1024',
+ key=cover_image_hash,
+ animated=False,
+ )
@classmethod
- def _from_guild_icon(cls, state, guild, *, format=None, static_format='webp', size=1024):
- if not utils.valid_icon_size(size):
- raise InvalidArgument("size must be a power of 2 between 16 and 4096")
- if format is not None and format not in VALID_AVATAR_FORMATS:
- raise InvalidArgument(f"format must be one of {VALID_AVATAR_FORMATS}")
- if format == "gif" and not guild.is_icon_animated():
- 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}')
+ def _from_guild_image(cls, state, guild_id: int, image: str, path: str) -> Asset:
+ return cls(
+ state,
+ url=f'{cls.BASE}/{path}/{guild_id}/{image}.png?size=1024',
+ key=image,
+ animated=False,
+ )
@classmethod
- def _from_sticker_url(cls, state, sticker, *, size=1024):
- if not utils.valid_icon_size(size):
- raise InvalidArgument("size must be a power of 2 between 16 and 4096")
+ def _from_guild_icon(cls, state, guild_id: int, icon_hash: str) -> Asset:
+ animated = icon_hash.startswith('a_')
+ 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
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}')
- def __str__(self):
- return self.BASE + self._url if self._url is not None else ''
-
- def __len__(self):
- if self._url:
- return len(self.BASE + self._url)
- return 0
+ def __str__(self) -> str:
+ return self._url
- def __bool__(self):
- return self._url is not None
+ def __len__(self) -> int:
+ return len(self._url)
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):
return isinstance(other, Asset) and self._url == other._url
- def __ne__(self, other):
- return not self.__eq__(other)
-
def __hash__(self):
return hash(self._url)
- async def read(self):
- """|coro|
+ @property
+ 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,
- and a URL won't be present if a custom image isn't associated with
- the asset, e.g. a guild with no custom icon.
+ def replace(
+ self,
+ 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
Raises
------
DiscordException
- There was no valid URL or internal connection state.
+ There was no internal connection state.
HTTPException
Downloading the asset failed.
NotFound
@@ -222,15 +354,12 @@ class Asset:
:class:`bytes`
The content of the asset.
"""
- if not self._url:
- raise DiscordException('Invalid asset (no URL provided)')
-
if self._state is None:
raise DiscordException('Invalid state (no ConnectionState provided)')
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|
Saves this asset into a file-like object.
@@ -245,7 +374,7 @@ class Asset:
Raises
------
DiscordException
- There was no valid URL or internal connection state.
+ There was no internal connection state.
HTTPException
Downloading the asset failed.
NotFound
diff --git a/discord/channel.py b/discord/channel.py
index ee6869c0..57ec56ca 100644
--- a/discord/channel.py
+++ b/discord/channel.py
@@ -94,9 +94,9 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
:attr:`~Permissions.manage_messages` bypass slowmode.
nsfw: :class:`bool`
If the channel is marked as "not safe for work".
-
+
.. note::
-
+
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.
nsfw: :class:`bool`
If the channel is marked as "not safe for work".
-
+
.. note::
-
+
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.
nsfw: :class:`bool`
If the channel is marked as "not safe for work".
-
+
.. note::
-
+
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',
@@ -1343,13 +1343,11 @@ class GroupChannel(discord.abc.Messageable, Hashable):
The group channel ID.
owner: :class:`User`
The user that owns the group channel.
- icon: Optional[:class:`str`]
- The group channel's icon hash if provided.
name: Optional[:class:`str`]
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):
self._state = state
@@ -1359,7 +1357,7 @@ class GroupChannel(discord.abc.Messageable, Hashable):
def _update_group(self, data):
owner_id = utils._get_as_snowflake(data, 'owner_id')
- self.icon = data.get('icon')
+ self._icon = data.get('icon')
self.name = data.get('name')
try:
@@ -1393,40 +1391,11 @@ class GroupChannel(discord.abc.Messageable, Hashable):
return ChannelType.group
@property
- def icon_url(self):
- """:class:`Asset`: Returns the channel's icon asset if available.
-
- This is equivalent to calling :meth:`icon_url_as` with
- the default parameters ('webp' format and a size of 1024).
- """
- 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)
+ def icon(self):
+ """Optional[:class:`Asset`]: Returns the channel's icon asset if available."""
+ if self._icon is None:
+ return None
+ return Asset._from_icon(self._state, self.id, self._icon, path='channel')
@property
def created_at(self):
diff --git a/discord/emoji.py b/discord/emoji.py
index bf7982ab..aba6d765 100644
--- a/discord/emoji.py
+++ b/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.
"""
+import io
+
from .asset import Asset
from . import utils
from .partial_emoji import _EmojiTag
@@ -132,13 +134,10 @@ class Emoji(_EmojiTag):
return utils.snowflake_time(self.id)
@property
- def url(self):
- """:class:`Asset`: Returns the asset of the emoji.
-
- This is equivalent to calling :meth:`url_as` with
- the default parameters (i.e. png/gif detection).
- """
- return self.url_as(format=None)
+ def url(self) -> str:
+ """:class:`str`: Returns the URL of the emoji."""
+ fmt = 'gif' if self.animated else 'png'
+ return f'{Asset.BASE}/emojis/{self.id}.{fmt}'
@property
def roles(self):
@@ -157,39 +156,6 @@ class Emoji(_EmojiTag):
""":class:`Guild`: The guild this emoji belongs to."""
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):
""":class:`bool`: Whether the bot can use this emoji.
@@ -254,3 +220,61 @@ class Emoji(_EmojiTag):
if 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)
+
+ 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)
diff --git a/discord/flags.py b/discord/flags.py
index cb268d09..5e182f8f 100644
--- a/discord/flags.py
+++ b/discord/flags.py
@@ -504,7 +504,7 @@ class Intents(BaseFlags):
- :attr:`Member.nick`
- :attr:`Member.premium_since`
- :attr:`User.name`
- - :attr:`User.avatar` (:attr:`User.avatar_url` and :meth:`User.avatar_url_as`)
+ - :attr:`User.avatar`
- :attr:`User.discriminator`
For more information go to the :ref:`member intent documentation <need_members_intent>`.
diff --git a/discord/guild.py b/discord/guild.py
index 3f63d873..20eff90b 100644
--- a/discord/guild.py
+++ b/discord/guild.py
@@ -94,8 +94,6 @@ class Guild(Hashable):
The timeout to get sent to the AFK channel.
afk_channel: Optional[:class:`VoiceChannel`]
The channel that denotes the AFK channel. ``None`` if it doesn't exist.
- icon: Optional[:class:`str`]
- The guild's icon.
id: :class:`int`
The guild's ID.
owner_id: :class:`int`
@@ -118,8 +116,6 @@ class Guild(Hashable):
The maximum amount of users in a video channel.
.. versionadded:: 1.4
- banner: Optional[:class:`str`]
- The guild's banner.
description: Optional[:class:`str`]
The guild's description.
mfa_level: :class:`int`
@@ -154,8 +150,6 @@ class Guild(Hashable):
- ``MEMBER_VERIFICATION_GATE_ENABLED``: Guild has Membership Screening enabled.
- ``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`
The premium tier for this guild. Corresponds to "Nitro Server" in the official UI.
The number goes from 0 to 3 inclusive.
@@ -164,10 +158,6 @@ class Guild(Hashable):
preferred_locale: Optional[:class:`str`]
The preferred locale for the guild. Used when filtering Server Discovery
results to a specific language.
- discovery_splash: :class:`str`
- The guild's discovery splash.
-
- .. versionadded:: 1.3
nsfw: :class:`bool`
If the guild is marked as "not safe for work".
@@ -175,15 +165,15 @@ class Guild(Hashable):
.. versionadded:: 2.0
"""
- __slots__ = ('afk_timeout', 'afk_channel', '_members', '_channels', 'icon',
- 'name', 'id', 'unavailable', 'banner', 'region', '_state',
+ __slots__ = ('afk_timeout', 'afk_channel', '_members', '_channels', '_icon',
+ 'name', 'id', 'unavailable', '_banner', 'region', '_state',
'_roles', '_member_count', '_large',
'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',
'description', 'max_presences', 'max_members', 'max_video_channel_users',
'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')
_PREMIUM_GUILD_LIMITS = {
@@ -293,8 +283,8 @@ class Guild(Hashable):
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.afk_timeout = guild.get('afk_timeout')
- self.icon = guild.get('icon')
- self.banner = guild.get('banner')
+ self._icon = guild.get('icon')
+ self._banner = guild.get('banner')
self.unavailable = guild.get('unavailable', False)
self.id = int(guild['id'])
self._roles = {}
@@ -306,7 +296,7 @@ class Guild(Hashable):
self.mfa_level = guild.get('mfa_level')
self.emojis = tuple(map(lambda d: state.store_emoji(self, d), guild.get('emojis', [])))
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.description = guild.get('description')
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._system_channel_flags = guild.get('system_channel_flags', 0)
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._public_updates_channel_id = utils._get_as_snowflake(guild, 'public_updates_channel_id')
self.nsfw = guild.get('nsfw', False)
@@ -621,139 +611,32 @@ class Guild(Hashable):
return self.get_member(self.owner_id)
@property
- def icon_url(self):
- """:class:`Asset`: Returns the guild's icon asset."""
- return self.icon_url_as()
-
- def is_icon_animated(self):
- """: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)
+ def icon(self):
+ """Optional[:class:`Asset`]: Returns the guild's icon asset, if available."""
+ if self._icon is None:
+ return None
+ return Asset._from_guild_icon(self._state, self.id, self._icon)
@property
- def banner_url(self):
- """:class:`Asset`: Returns the guild's banner asset."""
- return self.banner_url_as()
-
- def banner_url_as(self, *, format='webp', size=2048):
- """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)
+ def banner(self):
+ """Optional[:class:`Asset`]: Returns the guild's banner asset, if available."""
+ if self._banner is None:
+ return None
+ return Asset._from_guild_image(self._state, self.id, self._banner, path='banners')
@property
- def splash_url(self):
- """:class:`Asset`: Returns the guild's invite splash asset."""
- return self.splash_url_as()
-
- def splash_url_as(self, *, format='webp', size=2048):
- """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)
+ def splash(self):
+ """Optional[:class:`Asset`]: Returns the guild's invite splash asset, if available."""
+ if self._splash is None:
+ return None
+ return Asset._from_guild_image(self._state, self.id, self._splash, path='splashes')
@property
- def discovery_splash_url(self):
- """:class:`Asset`: Returns the guild's discovery splash asset.
-
- .. versionadded:: 1.3
- """
- 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)
+ def discovery_splash(self):
+ """Optional[:class:`Asset`]: Returns the guild's discovery splash asset, if available."""
+ if self._discovery_splash is None:
+ return None
+ return Asset._from_guild_image(self._state, self.id, self._discovery_splash, path='discovery-splashes')
@property
def member_count(self):
@@ -1185,7 +1068,7 @@ class Guild(Hashable):
try:
icon_bytes = fields['icon']
except KeyError:
- icon = self.icon
+ icon = self._icon
else:
if icon_bytes is not None:
icon = utils._bytes_to_base64_data(icon_bytes)
@@ -1195,7 +1078,7 @@ class Guild(Hashable):
try:
banner_bytes = fields['banner']
except KeyError:
- banner = self.banner
+ banner = self._banner
else:
if banner_bytes is not None:
banner = utils._bytes_to_base64_data(banner_bytes)
@@ -1212,7 +1095,7 @@ class Guild(Hashable):
try:
splash_bytes = fields['splash']
except KeyError:
- splash = self.splash
+ splash = self._splash
else:
if splash_bytes is not None:
splash = utils._bytes_to_base64_data(splash_bytes)
diff --git a/discord/invite.py b/discord/invite.py
index d8bd554f..ce601299 100644
--- a/discord/invite.py
+++ b/discord/invite.py
@@ -140,26 +140,20 @@ class PartialInviteGuild:
The partial guild's verification level.
features: List[:class:`str`]
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`]
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):
self._state = state
self.id = id
self.name = data['name']
self.features = data.get('features', [])
- self.icon = data.get('icon')
- self.banner = data.get('banner')
- self.splash = data.get('splash')
+ self._icon = data.get('icon')
+ self._banner = data.get('banner')
+ self._splash = data.get('splash')
self.verification_level = try_enum(VerificationLevel, data.get('verification_level'))
self.description = data.get('description')
@@ -178,56 +172,25 @@ class PartialInviteGuild:
return snowflake_time(self.id)
@property
- def icon_url(self) -> Asset:
- """:class:`Asset`: Returns the guild's icon asset."""
- return self.icon_url_as()
-
- def is_icon_animated(self) -> bool:
- """: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)
+ def icon_url(self) -> Optional[Asset]:
+ """Optional[:class:`Asset`]: Returns the guild's icon asset, if available."""
+ if self._icon is None:
+ return None
+ return Asset._from_guild_icon(self._state, self.id, self._icon)
@property
- def banner_url(self) -> Asset:
- """:class:`Asset`: Returns the guild's banner asset."""
- return self.banner_url_as()
-
- def banner_url_as(self, *, format='webp', size=2048) -> Asset:
- """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)
+ def banner(self) -> Optional[Asset]:
+ """Optional[:class:`Asset`]: Returns the guild's banner asset, if available."""
+ if self._banner is None:
+ return None
+ return Asset._from_guild_image(self._state, self.id, self._banner, path='banners')
@property
- def splash_url(self) -> Asset:
- """:class:`Asset`: Returns the guild's invite splash asset."""
- return self.splash_url_as()
-
- def splash_url_as(self, *, format='webp', size=2048) -> Asset:
- """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)
+ def splash(self) -> Optional[Asset]:
+ """Optional[:class:`Asset`]: Returns the guild's invite splash asset, if available."""
+ if self._splash is None:
+ return None
+ return Asset._from_guild_image(self._state, self.id, self._splash, path='splashes')
class Invite(Hashable):
diff --git a/discord/member.py b/discord/member.py
index 01cb299c..b6f98282 100644
--- a/discord/member.py
+++ b/discord/member.py
@@ -325,12 +325,12 @@ class Member(discord.abc.Messageable, _BaseUser):
def _update_inner_user(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
modified = (user['username'], user['avatar'], user['discriminator'], user.get('public_flags', 0))
if original != modified:
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
return to_return, u
@@ -442,7 +442,7 @@ class Member(discord.abc.Messageable, _BaseUser):
.. 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
than 128 characters. See :issue:`1738` for more information.
diff --git a/discord/message.py b/discord/message.py
index eadd03e5..97d42704 100644
--- a/discord/message.py
+++ b/discord/message.py
@@ -172,11 +172,11 @@ class Attachment(Hashable):
The number of bytes written.
"""
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)
if seek_begin:
fp.seek(0)
- return written
+ return written or 0
else:
with open(fp, 'wb') as f:
return f.write(data)
diff --git a/discord/partial_emoji.py b/discord/partial_emoji.py
index c84da4bf..0c65dc34 100644
--- a/discord/partial_emoji.py
+++ b/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.
"""
+import io
+
from .asset import Asset
+from .errors import DiscordException, InvalidArgument
from . import utils
__all__ = (
@@ -149,44 +152,83 @@ class PartialEmoji(_EmojiTag):
return utils.snowflake_time(self.id)
@property
- def url(self):
- """:class:`Asset`: Returns the asset of the emoji, if it is custom.
+ def url(self) -> str:
+ """:class:`str`: Returns the URL of the emoji, if it is custom.
- This is equivalent to calling :meth:`url_as` with
- the default parameters (i.e. png/gif detection).
+ If this isn't a custom emoji then an empty string is returned
"""
- return self.url_as(format=None)
+ if self.is_unicode_emoji():
+ return ''
- def url_as(self, *, format=None, static_format="png"):
- """Returns an :class:`Asset` for the emoji's url, if it is custom.
+ fmt = 'gif' if self.animated else 'png'
+ return f'{Asset.BASE}/emojis/{self.id}.{fmt}'
- The format must be one of 'webp', 'jpeg', 'jpg', 'png' or 'gif'.
- 'gif' is only valid for animated emojis.
+ async def read(self):
+ """|coro|
- .. versionadded:: 1.7
+ Retrieves the content of this emoji as a :class:`bytes` object.
- 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'
+ .. versionadded:: 2.0
Raises
- -------
+ ------
+ DiscordException
+ There was no internal connection state.
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
- --------
- :class:`Asset`
- The resulting CDN asset.
+ -------
+ :class:`bytes`
+ The content of the emoji.
"""
+ if self._state is None:
+ raise DiscordException('Invalid state (no ConnectionState provided)')
+
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)
diff --git a/discord/sticker.py b/discord/sticker.py
index 0b3c66d9..8a1e4518 100644
--- a/discord/sticker.py
+++ b/discord/sticker.py
@@ -62,12 +62,10 @@ class Sticker(Hashable):
The id of the sticker's pack.
format: :class:`StickerType`
The format for the sticker's image.
- image: :class:`str`
- The sticker's image.
tags: List[:class:`str`]
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):
self._state = state
@@ -76,7 +74,7 @@ class Sticker(Hashable):
self.description = data['description']
self.pack_id = int(data['pack_id'])
self.format = try_enum(StickerType, data['format_type'])
- self.image = data['asset']
+ self._image = data['asset']
try:
self.tags = [tag.strip() for tag in data['tags'].split(',')]
@@ -95,7 +93,7 @@ class Sticker(Hashable):
return snowflake_time(self.id)
@property
- def image_url(self):
+ def image(self):
"""Returns an :class:`Asset` for the sticker's image.
.. note::
@@ -106,32 +104,7 @@ class Sticker(Hashable):
Optional[:class:`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:
return None
- return Asset._from_sticker_url(self._state, self, size=size)
+ return Asset._from_sticker_url(self._state, self.id, self._image)
diff --git a/discord/team.py b/discord/team.py
index 3278433a..5e47770b 100644
--- a/discord/team.py
+++ b/discord/team.py
@@ -42,8 +42,6 @@ class Team:
The team ID.
name: :class:`str`
The team name
- icon: Optional[:class:`str`]
- The icon hash, if it exists.
owner_id: :class:`int`
The team's owner ID.
members: List[:class:`TeamMember`]
@@ -52,14 +50,14 @@ class Team:
.. versionadded:: 1.3
"""
- __slots__ = ('_state', 'id', 'name', 'icon', 'owner_id', 'members')
+ __slots__ = ('_state', 'id', 'name', '_icon', 'owner_id', 'members')
def __init__(self, state, data):
self._state = state
- self.id = utils._get_as_snowflake(data, 'id')
+ self.id = int(data['id'])
self.name = data['name']
- self.icon = data['icon']
+ self._icon = data['icon']
self.owner_id = utils._get_as_snowflake(data, 'owner_user_id')
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}>'
@property
- def icon_url(self):
- """:class:`.Asset`: Retrieves the team's icon asset.
-
- This is equivalent to calling :meth:`icon_url_as` with
- the default parameters ('webp' format and a size of 1024).
- """
- 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)
+ def icon(self):
+ """Optional[:class:`.Asset`]: Retrieves the team's icon asset, if any."""
+ if self._icon is None:
+ return None
+ return Asset._from_icon(self._state, self.id, self._icon, path='team')
@property
def owner(self):
diff --git a/discord/user.py b/discord/user.py
index 9f55b4ac..888c5c95 100644
--- a/discord/user.py
+++ b/discord/user.py
@@ -38,7 +38,7 @@ _BaseUser = discord.abc.User
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):
self._state = state
@@ -60,7 +60,7 @@ class BaseUser(_BaseUser):
self.name = data['username']
self.id = int(data['id'])
self.discriminator = data['discriminator']
- self.avatar = data['avatar']
+ self._avatar = data['avatar']
self._public_flags = data.get('public_flags', 0)
self.bot = data.get('bot', False)
self.system = data.get('system', False)
@@ -72,7 +72,7 @@ class BaseUser(_BaseUser):
self.name = user.name
self.id = user.id
self.discriminator = user.discriminator
- self.avatar = user.avatar
+ self._avatar = user._avatar
self.bot = user.bot
self._state = user._state
self._public_flags = user._public_flags
@@ -83,7 +83,7 @@ class BaseUser(_BaseUser):
return {
'username': self.name,
'id': self.id,
- 'avatar': self.avatar,
+ 'avatar': self._avatar,
'discriminator': self.discriminator,
'bot': self.bot,
}
@@ -94,66 +94,20 @@ class BaseUser(_BaseUser):
return PublicUserFlags._from_value(self._public_flags)
@property
- def avatar_url(self):
+ def avatar(self):
""":class:`Asset`: 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.
-
- 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
def default_avatar(self):
- """:class:`DefaultAvatar`: 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))
-
- @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')
+ """:class:`Asset`: Returns the default avatar for a given user. This is calculated by the user's discriminator."""
+ return Asset._from_default_avatar(self._state, int(self.discriminator) % len(DefaultAvatar))
@property
def colour(self):
diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py
index aadc752c..807eb1a0 100644
--- a/discord/webhook/async_.py
+++ b/discord/webhook/async_.py
@@ -475,56 +475,23 @@ class PartialWebhookGuild(Hashable):
The partial guild's icon
"""
- __slots__ = ('id', 'name', 'icon', '_state')
+ __slots__ = ('id', 'name', '_icon', '_state')
def __init__(self, *, data, state):
self._state = state
self.id = int(data['id'])
self.name = data['name']
- self.icon = data['icon']
+ self._icon = data['icon']
def __repr__(self):
return f'<PartialWebhookGuild name={self.name!r} id={self.id}>'
@property
- def icon_url(self) -> Asset:
- """:class:`Asset`: Returns the guild's icon asset."""
- return self.icon_url_as()
-
- def is_icon_animated(self) -> bool:
- """: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)
+ def icon_url(self) -> Optional[Asset]:
+ """Optional[:class:`Asset`]: Returns the guild's icon asset, if available."""
+ if self._icon is None:
+ return None
+ return Asset._from_guild_icon(self._state, self.id, self._icon)
class _FriendlyHttpAttributeErrorHelper:
@@ -684,7 +651,7 @@ class BaseWebhook(Hashable):
'auth_token',
'user',
'name',
- 'avatar',
+ '_avatar',
'source_channel',
'source_guild',
'_state',
@@ -701,7 +668,7 @@ class BaseWebhook(Hashable):
self.channel_id = utils._get_as_snowflake(data, 'channel_id')
self.guild_id = utils._get_as_snowflake(data, 'guild_id')
self.name = data.get('name')
- self.avatar = data.get('avatar')
+ self._avatar = data.get('avatar')
self.token = data.get('token')
user = data.get('user')
@@ -755,59 +722,16 @@ class BaseWebhook(Hashable):
return utils.snowflake_time(self.id)
@property
- def avatar_url(self) -> Asset:
+ def avatar(self) -> Asset:
""":class:`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.
-
- 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
- return Asset(self._state, '/embed/avatars/0.png')
-
- 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)
-
+ return Asset._from_default_avatar(self._state, 0)
+ return Asset._from_avatar(self._state, self.id, self._avatar)
class Webhook(BaseWebhook):
"""Represents an asynchronous Discord webhook.
@@ -980,7 +904,7 @@ class Webhook(BaseWebhook):
'name': name,
'channel_id': channel.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