aboutsummaryrefslogtreecommitdiff
path: root/discord/asset.py
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 /discord/asset.py
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.
Diffstat (limited to 'discord/asset.py')
-rw-r--r--discord/asset.py339
1 files changed, 234 insertions, 105 deletions
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