aboutsummaryrefslogtreecommitdiff
path: root/discord/asset.py
diff options
context:
space:
mode:
authorNCPlayz <[email protected]>2019-03-21 19:59:58 +0000
committerRapptz <[email protected]>2019-04-06 19:12:50 -0400
commitbe227ebcf0c8bad6b56798339b5414b8da414dc0 (patch)
treec7ea93ffc51e9a490b42d36e5c734b6b19ec3909 /discord/asset.py
parentPropagate Cloudflare 429 HTML text. (diff)
downloaddiscord.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.py157
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)