diff options
| author | NCPlayz <[email protected]> | 2019-03-21 19:59:58 +0000 |
|---|---|---|
| committer | Rapptz <[email protected]> | 2019-04-06 19:12:50 -0400 |
| commit | be227ebcf0c8bad6b56798339b5414b8da414dc0 (patch) | |
| tree | c7ea93ffc51e9a490b42d36e5c734b6b19ec3909 /discord/asset.py | |
| parent | Propagate Cloudflare 429 HTML text. (diff) | |
| download | discord.py-be227ebcf0c8bad6b56798339b5414b8da414dc0.tar.xz discord.py-be227ebcf0c8bad6b56798339b5414b8da414dc0.zip | |
Redesign asset retrieval in the library.
Most assets now return a new class named `Asset`. This allows for the
assets to be consistently saved via a `save` method instead of special
casing for `Attachment`.
`AppInfo` is no longer a namedtuple it is a fully documented dataclass,
as well as having the state attached to it.
Fixes #1997
Diffstat (limited to 'discord/asset.py')
| -rw-r--r-- | discord/asset.py | 157 |
1 files changed, 157 insertions, 0 deletions
diff --git a/discord/asset.py b/discord/asset.py new file mode 100644 index 00000000..4e592f67 --- /dev/null +++ b/discord/asset.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- + +""" +The MIT License (MIT) + +Copyright (c) 2015-2019 Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +import io +from .errors import DiscordException +from .errors import InvalidArgument +from . import utils + +VALID_STATIC_FORMATS = frozenset({"jpeg", "jpg", "webp", "png"}) +VALID_AVATAR_FORMATS = VALID_STATIC_FORMATS | {"gif"} + +class Asset: + """Represents a CDN asset on Discord. + + .. container:: operations + + .. describe:: str(x) + + Returns the URL of the CDN asset. + + .. describe:: len(x) + + Returns the length of the CDN asset's URL. + + .. describe:: bool(x) + + Checks if the Asset has a URL. + """ + __slots__ = ('_state', '_url') + + def __init__(self, state, url=None): + self._state = state + self._url = url + + @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 1024") + if format is not None and format not in VALID_AVATAR_FORMATS: + raise InvalidArgument("format must be None or one of {}".format(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("static_format must be one of {}".format(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, 'https://cdn.discordapp.com/avatars/{0.id}/{0.avatar}.{1}?size={2}'.format(user, format, size)) + + @classmethod + def _from_icon(cls, state, object, path): + if object.icon is None: + return cls(state) + + url = 'https://cdn.discordapp.com/{0}-icons/{1.id}/{1.icon}.jpg'.format(path, object) + return cls(state, url) + + @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("format must be one of {}".format(VALID_STATIC_FORMATS)) + + if hash is None: + return Asset(state) + + url = 'https://cdn.discordapp.com/{key}/{0}/{1}.{2}?size={3}' + return cls(state, url.format(id, hash, format, size, key=key)) + + def __str__(self): + return self._url + + def __len__(self): + return len(self._url) + + def __bool__(self): + return self._url is not None + + def __repr__(self): + return '<Asset url={0._url!r}>'.format(self) + + async def save(self, fp, *, seek_begin=True): + """|coro| + + Saves this asset into a file-like object. + + 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 valid URL or internal connection state. + + .. note:: + + :class:`PartialEmoji` will not have a state if you make + your own instance via ``PartialEmoji(animated=False, name='x', id=2345678)``. + + The URL will not be provided if there is no custom image. + HTTPException + Saving the asset failed. + NotFound + The asset was deleted. + + Returns + -------- + :class:`int` + The number of bytes written. + """ + if not self._url: + raise DiscordException('Invalid asset (no URL provided)') + + if self._state is None: + raise DiscordException('Invalid state (no ConnectionState provided)') + + data = await self._state.http.get_from_cdn(self._url) + 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) |