mirror of
https://github.com/Rapptz/discord.py.git
synced 2025-09-06 09:56:09 +00:00
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.
This commit is contained in:
339
discord/asset.py
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.
|
||||
"""
|
||||
|
||||
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 __str__(self) -> str:
|
||||
return self._url
|
||||
|
||||
def __len__(self):
|
||||
if self._url:
|
||||
return len(self.BASE + self._url)
|
||||
return 0
|
||||
|
||||
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):
|
||||
@property
|
||||
def url(self) -> str:
|
||||
""":class:`str`: Returns the underlying URL of the asset."""
|
||||
return self._url
|
||||
|
||||
@property
|
||||
def key(self) -> str:
|
||||
""":class:`str`: Returns the identifying key of the asset."""
|
||||
return self._key
|
||||
|
||||
def is_animated(self) -> bool:
|
||||
""":class:`bool`: Returns whether the asset is animated."""
|
||||
return self._animated
|
||||
|
||||
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.
|
||||
|
||||
.. warning::
|
||||
|
||||
: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.
|
||||
|
||||
.. 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
|
||||
|
Reference in New Issue
Block a user