diff options
| author | Rapptz <[email protected]> | 2021-04-16 11:21:13 -0400 |
|---|---|---|
| committer | Rapptz <[email protected]> | 2021-04-16 11:27:23 -0400 |
| commit | 9eaf1e85e4e987b5f874a7ba4c3ed13de10fd154 (patch) | |
| tree | 83a60b0aaff3c1b63868631418cb7583adda054d /discord/asset.py | |
| parent | Add `fetch_message` for webhooks (diff) | |
| download | discord.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.py | 339 |
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 |